diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..aef69ea --- /dev/null +++ b/.coveragerc @@ -0,0 +1,11 @@ +[run] +branch = True +source = . +omit = + tests/* + .tox/* + setup.py + venv/* + time_trials/* + doctests.py + diff --git a/.gitignore b/.gitignore index b456e0f..ab3c2a1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,10 +11,10 @@ /*.egg # other -tox.ini *.npy *.spec *.txt +.coverage .tox .idea /time_trials/best_time_data @@ -24,3 +24,5 @@ tox.ini /docs/_build /_build run_sphinx.sh +venv/ + diff --git a/.travis.yml b/.travis.yml index b420a34..d5eb840 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,12 @@ install: script: - - coverage run --source=dicetables setup.py test + - COVERALLS_PARALLEL=true + - coverage run -a --source=dicetables setup.py test - coverage report -m # Run the doctests - python doctests.py after_success: + - COVERALLS_PARALLEL=true - coveralls diff --git a/README.rst b/README.rst index 2af4a2c..83a494c 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ ################# -dicetables v2.5.0 +dicetables v2.6.0 ################# Calculate the Combinations For Any Set of Dice @@ -139,6 +139,13 @@ StatsStrings(query_values='8-19, 35, 50-62', 5999: 1,000 6000: 1 +You can now roll events with a `Roller` + +>>> events = dt.DiceTable.new().add_die(dt.Die(6)) +>>> roller = dt.Roller(events) +>>> roller.roll() in [1, 2, 3, 4, 5, 6] +True + That should get you started. For details see ``_ @@ -180,6 +187,10 @@ v2.5.0 - `Parser().add_die_size_limit_kwarg` and `Parser().add_explosions_limit_kwarg` are removed. Use `Parser().add_limits_kwarg` +v2.6.0 + +- added `Roller` + .. _`The Dice` : http://dice-tables.readthedocs.io/en/latest/the_dice.html .. _`DicePool` : http://dice-tables.readthedocs.io/en/latest/the_dice.html#module-dicetables.bestworstmid diff --git a/dicetables/__init__.py b/dicetables/__init__.py index 18614a9..40e3062 100644 --- a/dicetables/__init__.py +++ b/dicetables/__init__.py @@ -1,12 +1,13 @@ from __future__ import absolute_import from dicetables.additiveevents import AdditiveEvents +from dicetables.bestworstmid import BestOfDicePool, WorstOfDicePool, UpperMidOfDicePool, LowerMidOfDicePool +from dicetables.dicerecord import DiceRecord from dicetables.dicetable import DiceTable, DetailedDiceTable from dicetables.dieevents import Die, ModDie, WeightedDie, ModWeightedDie, StrongDie, Modifier, ExplodingOn, Exploding -from dicetables.dicerecord import DiceRecord from dicetables.eventsbases.eventerrors import DiceRecordError, InvalidEventsError -from dicetables.parser import Parser, ParseError, LimitsError from dicetables.eventsinfo import (EventsCalculations, EventsInformation, events_range, mean, stddev, percentage_points, percentage_axes, stats, full_table_string) -from dicetables.bestworstmid import BestOfDicePool, WorstOfDicePool, UpperMidOfDicePool, LowerMidOfDicePool +from dicetables.parser import Parser, ParseError, LimitsError +from dicetables.roller import Roller diff --git a/dicetables/additiveevents.py b/dicetables/additiveevents.py index f80bc73..6d610d3 100644 --- a/dicetables/additiveevents.py +++ b/dicetables/additiveevents.py @@ -3,10 +3,9 @@ """ from __future__ import absolute_import - +from dicetables.eventsbases.integerevents import IntegerEvents from dicetables.factory.eventsfactory import EventsFactory from dicetables.tools.dictcombiner import DictCombiner -from dicetables.eventsbases.integerevents import IntegerEvents def scrub_zeroes(dictionary): diff --git a/dicetables/bestworstmid.py b/dicetables/bestworstmid.py index 571bd90..96d698a 100644 --- a/dicetables/bestworstmid.py +++ b/dicetables/bestworstmid.py @@ -1,6 +1,5 @@ -from dicetables.tools.orderedcombinations import ordered_combinations_of_events - from dicetables.dieevents import ProtoDie +from dicetables.tools.orderedcombinations import ordered_combinations_of_events class DicePool(ProtoDie): @@ -9,6 +8,7 @@ class DicePool(ProtoDie): rolls are selected from the pool of total rolls. Different implementations determine which particular rolls to select. """ + def __init__(self, input_die, pool_size, select): """ @@ -65,6 +65,7 @@ class BestOfDicePool(DicePool): Take the best [select] rolls from a pool of [pool_size] * [input_die]. BestOfDicePool(Die(6), 4, 3) is the best 3 rolls from four six-sided dice. """ + def __init__(self, input_die, pool_size, select): super(BestOfDicePool, self).__init__(input_die, pool_size, select) @@ -80,6 +81,7 @@ class WorstOfDicePool(DicePool): Take the worst [select] rolls from a pool of [pool_size] * [input_die]. WorstOfDicePool(Die(6), 4, 3) is the worst 3 rolls from four six-sided dice. """ + def __init__(self, input_die, pool_size, select): super(WorstOfDicePool, self).__init__(input_die, pool_size, select) @@ -98,6 +100,7 @@ class UpperMidOfDicePool(DicePool): If there is no perfect middle, take the higher of two choices. For five dice that roll (1, 1, 2, 3, 4), select=3 takes (1, 2, 3) and select=2 takes (2, 3). """ + def __init__(self, input_die, pool_size, select): super(UpperMidOfDicePool, self).__init__(input_die, pool_size, select) @@ -117,6 +120,7 @@ class LowerMidOfDicePool(DicePool): If there is no perfect middle, take the lower of two choices. For five dice that roll (1, 1, 2, 3, 4), select=3 takes (1, 2, 3) and select=2 takes (1, 2). """ + def __init__(self, input_die, pool_size, select): super(LowerMidOfDicePool, self).__init__(input_die, pool_size, select) diff --git a/dicetables/dicerecord.py b/dicetables/dicerecord.py index 8a0d3e3..21844cc 100644 --- a/dicetables/dicerecord.py +++ b/dicetables/dicerecord.py @@ -3,9 +3,8 @@ """ from sys import version_info -from dicetables.eventsbases.protodie import ProtoDie from dicetables.eventsbases.eventerrors import DiceRecordError - +from dicetables.eventsbases.protodie import ProtoDie if version_info[0] < 3: from dicetables.tools.py2funcs import is_int diff --git a/dicetables/dicetable.py b/dicetables/dicetable.py index d641778..e328950 100644 --- a/dicetables/dicetable.py +++ b/dicetables/dicetable.py @@ -3,7 +3,6 @@ """ from __future__ import absolute_import - from dicetables.additiveevents import AdditiveEvents, EventsDictCreator from dicetables.eventsinfo import EventsCalculations from dicetables.factory.eventsfactory import EventsFactory diff --git a/dicetables/dieevents.py b/dicetables/dieevents.py index adadfae..b19c2a5 100644 --- a/dicetables/dieevents.py +++ b/dicetables/dieevents.py @@ -3,7 +3,6 @@ """ import itertools - from dicetables.eventsbases.protodie import ProtoDie @@ -13,6 +12,7 @@ class Modifier(ProtoDie): :code:`Modifier(-3)` rolls -3 and only -3. A Modifier's size and weight are always 0. """ + def __init__(self, modifier): self._mod = modifier super(Modifier, self).__init__() @@ -47,6 +47,7 @@ class Die(ProtoDie): stores and returns info for a basic Die. :code:`Die(4)` rolls 1, 2, 3, 4 with equal weight """ + def __init__(self, die_size): """ @@ -83,6 +84,7 @@ class ModDie(Die): that changes the values of the rolls. :code:`ModDie(4, -1)` rolls 0, 1, 2, 3 with equal weight """ + def __init__(self, die_size, modifier): """ @@ -113,6 +115,7 @@ class WeightedDie(ProtoDie): stores and returns info for die with different chances for different rolls. :code:`WeightedDie({1:1, 2:5})` rolls 1 once for every five times that 2 is rolled. """ + def __init__(self, dictionary_input): """ @@ -350,6 +353,7 @@ class ExplodingOn(ProtoDie): instantiation VERY slow. Time is proportional to explosion**(len(explodes_on)). It's also linear with size which gets overshadowed by the first factor. """ + def __init__(self, input_die, explodes_on, explosions=2): """ diff --git a/dicetables/eventsbases/protodie.py b/dicetables/eventsbases/protodie.py index da25d86..dcd8fac 100644 --- a/dicetables/eventsbases/protodie.py +++ b/dicetables/eventsbases/protodie.py @@ -18,6 +18,7 @@ class ProtoDie(IntegerEvents): - __repr__ - __str__ """ + def __init__(self): super(ProtoDie, self).__init__() @@ -53,8 +54,8 @@ def __hash__(self): def __lt__(self, other): return ( - (self.get_size(), self.get_weight(), sorted(self.get_dict().items()), repr(self)) < - (other.get_size(), other.get_weight(), sorted(other.get_dict().items()), repr(other)) + (self.get_size(), self.get_weight(), sorted(self.get_dict().items()), repr(self)) < + (other.get_size(), other.get_weight(), sorted(other.get_dict().items()), repr(other)) ) def __eq__(self, other): diff --git a/dicetables/eventsinfo.py b/dicetables/eventsinfo.py index 97bfbe6..02a341a 100644 --- a/dicetables/eventsinfo.py +++ b/dicetables/eventsinfo.py @@ -11,14 +11,15 @@ Can be accessed through objects or wrapper functions. """ - from __future__ import absolute_import +from collections import namedtuple from decimal import Decimal + from math import log10 -from collections import namedtuple -from dicetables.tools.numberforamtter import NumberFormatter + from dicetables.tools.listtostring import get_string_from_list_of_ints +from dicetables.tools.numberforamtter import NumberFormatter def safe_true_div(numerator, denominator): @@ -69,7 +70,7 @@ def biggest_event(self): :return: (event, occurrences) for first event with highest occurrences """ highest_occurrences = max(self._dict.values()) - for event, occurrences in sorted(self._dict.items()): + for event, occurrences in sorted(self._dict.items()): # pragma: no branch if occurrences == highest_occurrences: return event, highest_occurrences @@ -80,7 +81,7 @@ def biggest_events_all(self): """ highest_occurrences = max(self._dict.values()) output = [] - for event, occurrences in sorted(self._dict.items()): + for event, occurrences in self._dict.items(): if occurrences == highest_occurrences: output.append((event, occurrences)) return sorted(output) diff --git a/dicetables/factory/errorhandler.py b/dicetables/factory/errorhandler.py index 2e58c36..5574455 100644 --- a/dicetables/factory/errorhandler.py +++ b/dicetables/factory/errorhandler.py @@ -1,5 +1,3 @@ - - class EventsFactoryError(AssertionError): pass @@ -26,7 +24,7 @@ def create_format_kwargs(self, error_code, params): } """ self._assign_parameters(error_code, params) - self._assign_additional_kwargs(error_code) + self._assign_kwargs_from_factory_class() def _assign_parameters(self, error_code, params): param_values = { @@ -39,26 +37,18 @@ def _assign_parameters(self, error_code, params): for index, key_name in enumerate(param_values[error_code]): self._format_kwargs[key_name] = params[index] - def _assign_additional_kwargs(self, error_code): - additional_params = { - 'CLASS OVERWRITE': 'class_keys', - 'MISSING GETTER': 'current_getters', - 'GETTER OVERWRITE': 'factory_getter', - 'SIGNATURES DIFFERENT': 'class_keys', - 'WTF': 'factory_classes' + def _assign_kwargs_from_factory_class(self): + factory_classes, current_getters = self._factory.get_keys() + kwargs_from_factory = { + 'factory_classes': factory_classes, + 'current_getters': current_getters } - new_key = additional_params[error_code] - new_value = None - if new_key == 'class_keys': - new_value = self._factory.get_class_params(self._format_kwargs['events_class']) - elif new_key == 'factory_getter': - new_value = self._factory.get_getter_string(self._format_kwargs['getter_key']) - elif new_key == 'factory_classes': - new_value = self._factory.get_keys()[0] - elif new_key == 'current_getters': - new_value = self._factory.get_keys()[1] - - self._format_kwargs[new_key] = new_value + if 'events_class' in self._format_kwargs: + kwargs_from_factory['class_keys'] = self._factory.get_class_params(self._format_kwargs['events_class']) + if 'getter_key' in self._format_kwargs: + kwargs_from_factory['factory_getter'] = self._factory.get_getter_string(self._format_kwargs['getter_key']) + + self._format_kwargs.update(kwargs_from_factory) def create_header(self, error_code, bad_param): msg_start = 'Error Code: {}\nFactory: {}\nError At: '.format(error_code, self._factory) diff --git a/dicetables/factory/eventsfactory.py b/dicetables/factory/eventsfactory.py index ab7b385..c43b702 100644 --- a/dicetables/factory/eventsfactory.py +++ b/dicetables/factory/eventsfactory.py @@ -1,11 +1,11 @@ """ A factory class for creating instances of AdditiveEvent and its descendants. """ +from dicetables.dicerecord import DiceRecord from dicetables.eventsbases.eventerrors import InvalidEventsError, DiceRecordError from dicetables.factory.errorhandler import EventsFactoryErrorHandler from dicetables.factory.factorytools import StaticDict, Getter from dicetables.factory.warninghandler import EventsFactoryWarningHandler -from dicetables.dicerecord import DiceRecord class LoaderError(AttributeError): @@ -32,7 +32,6 @@ def load(self, new_class): class EventsFactory(object): - __default_getters = {'get_dict': Getter('get_dict', {0: 1}), 'dice_data': Getter('dice_data', DiceRecord.new()), 'calc_includes_zeroes': Getter('calc_includes_zeroes', True, is_property=True)} diff --git a/dicetables/factory/warninghandler.py b/dicetables/factory/warninghandler.py index f3829d5..af87cf4 100644 --- a/dicetables/factory/warninghandler.py +++ b/dicetables/factory/warninghandler.py @@ -1,4 +1,3 @@ - import warnings @@ -20,10 +19,10 @@ def raise_warning(self, warning_code, *params): def _get_msg_start(self, failed_class, warning_code): msg_start = ( - '\nfactory: {}\n'.format(self._factory) + - 'Warning code: {}\n'.format(warning_code) + - 'Failed to find/add the following class to the EventsFactory - \n' + - 'class: {}\n'.format(failed_class) + '\nfactory: {}\n'.format(self._factory) + + 'Warning code: {}\n'.format(warning_code) + + 'Failed to find/add the following class to the EventsFactory - \n' + + 'class: {}\n'.format(failed_class) ) return msg_start @@ -33,9 +32,9 @@ def _get_error_code_body(params, warning_code): if warning_code == 'CONSTRUCT': in_factory_class = params[1] msg_middle = ( - '\nClass found in factory: {}\n'.format(in_factory_class) + - 'attempted object construction using its signature. tried to return instance of original class.\n' + - 'If that had failed, returned instance of the class found in EventsFactory.\n\n' + '\nClass found in factory: {}\n'.format(in_factory_class) + + 'attempted object construction using its signature. tried to return instance of original class.\n' + + 'If that had failed, returned instance of the class found in EventsFactory.\n\n' ) if warning_code == 'CHECK': msg_middle = ( @@ -45,18 +44,17 @@ def _get_error_code_body(params, warning_code): def _get_solution_message(self): instructions = ( - 'SOLUTION:\n' + - ' class variable: factory_keys = (getter method/property names)\n' - ' current factory keys are: {}\n'.format(self._factory.get_keys()[1]) + - ' class variable: new_keys = [(info for each key not already in factory)]\n' + - ' Each tuple in "new_keys" is (getter_name, default_value, "property"/"method")\n' - 'ex:\n' + - ' NewClass(Something):\n' + - ' factory_keys = ("dice_data", "get_dict", "get_thingy", "label")\n' + - ' new_keys = [("get_thingy", 0, "method"),\n' + - ' ("label", "", "property")]\n\n' + - ' def __init__(self, events_dict, dice_list, new_thingy, label):\n' + - ' ....\n' + 'SOLUTION:\n' + + ' class variable: factory_keys = (getter method/property names)\n' + ' current factory keys are: {}\n'.format(self._factory.get_keys()[1]) + + ' class variable: new_keys = [(info for each key not already in factory)]\n' + + ' Each tuple in "new_keys" is (getter_name, default_value, "property"/"method")\n' + 'ex:\n' + + ' NewClass(Something):\n' + + ' factory_keys = ("dice_data", "get_dict", "get_thingy", "label")\n' + + ' new_keys = [("get_thingy", 0, "method"),\n' + + ' ("label", "", "property")]\n\n' + + ' def __init__(self, events_dict, dice_list, new_thingy, label):\n' + + ' ....\n' ) return instructions - diff --git a/dicetables/parser.py b/dicetables/parser.py index fd955cd..ac0cbe0 100644 --- a/dicetables/parser.py +++ b/dicetables/parser.py @@ -1,8 +1,8 @@ import ast -from dicetables.eventsbases.protodie import ProtoDie -from dicetables.dieevents import Die, ModDie, Modifier, ModWeightedDie, WeightedDie, StrongDie, Exploding, ExplodingOn from dicetables.bestworstmid import DicePool, BestOfDicePool, WorstOfDicePool, UpperMidOfDicePool, LowerMidOfDicePool +from dicetables.dieevents import Die, ModDie, Modifier, ModWeightedDie, WeightedDie, StrongDie, Exploding, ExplodingOn +from dicetables.eventsbases.protodie import ProtoDie from dicetables.tools.orderedcombinations import count_unique_combination_keys, largest_permitted_pool_size diff --git a/dicetables/roller.py b/dicetables/roller.py new file mode 100644 index 0000000..41486ab --- /dev/null +++ b/dicetables/roller.py @@ -0,0 +1,43 @@ +import random + +from dicetables.tools.alias_table import AliasTable + + +class Roller(object): + def __init__(self, events, random_generator=None): + """ + + :param events: any IntegerEvents + :param random_generator: any instance of random.Random. defaults to python default random instance + """ + self._alias_table = AliasTable(events.get_dict()) + self._random_generator = random_generator + if not self._random_generator: + self._random_generator = random + + @property + def alias_table(self): + """ + here is + `a nice explanation of alias tables: `_ + + :return: dicetables.tools.AliasTable + """ + return self._alias_table + + @property + def random_generator(self): + return self._random_generator + + def roll(self): + length = self._random_generator.randrange(self._alias_table.length) + height = self._random_generator.randrange(self._alias_table.height) + return self._alias_table.get(length, height) + + def roll_many(self, times): + """ + + :param times: int - how many times to roll + :return: [int,..] - the value of each roll + """ + return [self.roll() for _ in range(times)] diff --git a/dicetables/tools/alias_table.py b/dicetables/tools/alias_table.py new file mode 100644 index 0000000..2dd1961 --- /dev/null +++ b/dicetables/tools/alias_table.py @@ -0,0 +1,67 @@ +from collections import namedtuple + +Alias = namedtuple('Alias', ['primary', 'alternate', 'primary_height']) + + +class AliasTable(object): + """ + + here is + `a nice explanation of alias tables: `_ + + `Vose's algorithm `_ + + """ + + def __init__(self, input_dict): + self._height = sum(input_dict.values()) + self._length = len(input_dict) + self._aliases = self._create_aliases(input_dict) + + def _create_aliases(self, input_dict): + big_heights, small_heights = self._get_ge_height_and_le_height_lists(input_dict) + + alias_list = [] + while small_heights: + primary, primary_height = small_heights.pop() + alternate, alternate_height = big_heights.pop() + alias_list.append(Alias(primary=primary, alternate=alternate, primary_height=primary_height)) + new_alternate_height = alternate_height - (self._height - primary_height) + self._update_sorting_lists(alternate, new_alternate_height, big_heights, small_heights) + + while big_heights: + primary, _ = big_heights.pop() + alias_list.append(Alias(primary=primary, alternate=primary, primary_height=self._height)) + + return alias_list + + def _update_sorting_lists(self, event, event_height, big_heights, small_heights): + if event_height < self._height: + small_heights.append((event, event_height)) + else: + big_heights.append((event, event_height)) + + def _get_ge_height_and_le_height_lists(self, input_dict): + less_than_height = [] + greater_than_or_equal_height = [] + for event, frequency in sorted(input_dict.items()): + event_height = self._length * frequency + self._update_sorting_lists(event, event_height, greater_than_or_equal_height, less_than_height) + return greater_than_or_equal_height, less_than_height + + @property + def height(self): + return self._height + + @property + def length(self): + return self._length + + def to_list(self): + return self._aliases[:] + + def get(self, length, height): + alias = self._aliases[length] + if height >= alias.primary_height: + return alias.alternate + return alias.primary diff --git a/dicetables/tools/numberforamtter.py b/dicetables/tools/numberforamtter.py index eced0b5..4277222 100644 --- a/dicetables/tools/numberforamtter.py +++ b/dicetables/tools/numberforamtter.py @@ -7,9 +7,10 @@ """ from __future__ import absolute_import -from math import log10 from sys import version_info +from math import log10 + if version_info[0] < 3: from dicetables.tools.py2funcs import is_int else: diff --git a/dicetables/tools/orderedcombinations.py b/dicetables/tools/orderedcombinations.py index 0f76caa..f3282d2 100644 --- a/dicetables/tools/orderedcombinations.py +++ b/dicetables/tools/orderedcombinations.py @@ -1,4 +1,5 @@ from itertools import combinations_with_replacement + from math import factorial diff --git a/docs/conf.py b/docs/conf.py index 4d49fb9..0551166 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,9 +61,9 @@ # built documents. # # The short X.Y version. -version = '2.3.0' +version = '2.6.0' # The full version, including alpha/beta/rc tags. -release = '2.3.0' +release = '2.6.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/implementation_details/parser.rst b/docs/implementation_details/parser.rst index 3451431..25304ad 100644 --- a/docs/implementation_details/parser.rst +++ b/docs/implementation_details/parser.rst @@ -295,7 +295,7 @@ LimitsError: Max die_size: 500 Limits and DicePool Objects --------------------------- -DicePool can take a surprisingly long time to calculate. See :ref:`Dice Pools` for a proper explanation. +DicePool can take a surprisingly long time to calculate. See the :ref:`Dice-Pools-Section` for a proper explanation. Suffice it to say that the limits on any DicePool can be determined by :code:`len(input_die.get_dict())` and :code:`pool_size`. The parser uses a dictionary of {max_dict_len: max_unique_combination_keys} at :code:`Parser().max_dice_pool_combinations_per_dict_size`. This is determined from the input_die using, diff --git a/docs/index.rst b/docs/index.rst index b59e50a..8f12bcf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,6 +26,7 @@ Contents: the_dice.rst the_dicetable.rst events_info.rst + roller.rst implementation_details/index.rst diff --git a/docs/intro.rst b/docs/intro.rst index 642a4b9..0dba413 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -8,7 +8,7 @@ Rolling two six-sided dice provides a good introduction to the math behind die rolls. Below are all the possible ways to roll 2D6 grouped by the total roll:: - _2: 1-1 + 2: 1-1 3: 1-2, 2-1 4: 1-3, 3-1, 2-2 5: 1-4, 4-1, 2-3, 3-2 @@ -75,9 +75,9 @@ roll has an equal chance of occurring, so it's represented by the dictionary: To combine this with another six-sided die, you iterate through each die roll and create a new dictionary. When the second die rolls a "1", -it bumps all the rolls up by one, making: :code:`{2: 1 ,3: 1, 4: 1, 5: 1, 6: 1, 7: 1}`. When +it bumps all the rolls up by one, making: :code:`{1+1: 1 ,2+1: 1, 3+1: 1, 4+1: 1, 5+1: 1, 6+1: 1}`. When the second die rolls a "2" it bumps all the die rolls up by two, making: -:code:`{3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1}`. Keep doing this for each roll and add them up. +:code:`{1+2: 1 ,2+2: 1, 3+2: 1, 4+2: 1, 5+2: 1, 6+2: 1}`. Keep doing this for each roll and add them up. :: {2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1} @@ -92,10 +92,18 @@ the second die rolls a "2" it bumps all the die rolls up by two, making: This dictionary of events is the spread of combinations for 2D6. It is the same as the other representation in the previous section. "2" has one combination, "3" has two combinations and "7" has six combinations. -Using this method, adding another die to 2D6 becomes much more manageable. Simply take the +Using this method, adding another six-sided die to 2D6 becomes much more manageable. Simply take the dictionary of 2D6 above, and for each roll of the next six-sided die, create a new dictionary of events. -So for a roll of 2, create the dictionary: -:code:`{2+2: 1, 3+2: 2, 4+2: 3, 5+2: 4, 6+2: 5, 7+2: 6, 8+2: 5, 9+2: 4, 10+2: 3, 11+2: 2, 12+2: 1}` + +When the third six-sided die rolls a "1", you get:: + + {2+1: 1, 3+1: 2, 4+1: 3, 5+1: 4, 6+1: 5, 7+1: 6, 8+1: 5, 9+1: 4, 10+1: 3, 11+1: 2, 12+1: 1} + +And when it rolls a "2", you get:: + + + {2+2: 1, 3+2: 2, 4+2: 3, 5+2: 4, 6+2: 5, 7+2: 6, 8+2: 5, 9+2: 4, 10+2: 3, 11+2: 2, 12+2: 1} + This makes:: {3: 1, 4: 2, 5: 3, 6: 4, 7: 5, 8: 6, 9: 5, 10: 4, 11: 3, 12: 2, 13: 1} @@ -112,17 +120,30 @@ This makes:: Take 2d6 and add a different die to it. This time, it's a weighted 2-sided die that rolls "2" three times as often as "1". This die is represented by the dictionary: :code:`{1: 1, 2: 3}`. -For the :code:`{1: 1, ...`, you add 1 to each roll in your 2D6 events, like so: -:code:`{3: 1, 4: 2, 5: 3, 6: 4, 7: 5, 8: 6, 9: 5, 10: 4, 11: 3, 12: 2, 13: 1}`. -For the :code:`..., 2: 3}`, you add two to the die rolls **three times**, and each time is: -:code:`{4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1}` -this gives you:: +Two 6-sided dice:: + + {2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 5, 9: 4, 10: 3, 11: 2, 12: 1} + +A weighted 2-sided die:: + + {1: 1, 2: 3} + +Applying a roll of "one" to two 6-sided dice:: + + + A = {2+1: 1, 3+1: 2, 4+1: 3, 5+1: 4, 6+1: 5, 7+1: 6, 8+1: 5, 9+1: 4, 10+1: 3, 11+1: 2, 12+1: 1} + +Applying a roll of "two" to two 6-sided dice:: + + B = {2+2: 1, 3+2: 2, 4+2: 3, 5+2: 4, 6+2: 5, 7+2: 6, 8+2: 5, 9+2: 4, 10+2: 3, 11+2: 2, 12+2: 1} + +On this weighted die, "2" gets rolled 3 times as often as "1". So you combine `1*A` and `3*B`:: - {3: 1, 4: 2, 5: 3, 6: 4, 7: 5, 8: 6, 9: 5, 10: 4, 11: 3, 12: 2, 13: 1} - {4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1} - {4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1} - + {4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1} + {3: 1, 4: 2, 5: 3, 6: 4, 7: 5, 8: 6, 9: 5, 10: 4, 11: 3, 12: 2, 13: 1} # A + {4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1} # B + {4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1} # B + + {4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1} # B ----------------------------------------------------------------------------------------- {3: 1, 4: 5, 5: 9, 6: 13, 7: 17, 8: 21, 9: 23, 10: 19, 11: 15, 12: 11, 13: 7, 14: 3} diff --git a/docs/roller.rst b/docs/roller.rst new file mode 100644 index 0000000..4073d20 --- /dev/null +++ b/docs/roller.rst @@ -0,0 +1,50 @@ +Roller +====== + +A Roller performs random rolls on any `IntegerEvents`, including dice and DiceTables. +You can pass it your own random generator or let it use the python default random generator. +Python has specs on how to subclass `random.Random` `here +`_. + + Class Random can also be subclassed if you want to use a different basic generator of your own devising: + in that case, override the random(), seed(), getstate(), and setstate() methods. + Optionally, a new generator can supply a getrandbits() method — this allows randrange() + to produce selections over an arbitrarily large range. + +This roller relies on `random.randrange` and alias tables. Since it relies on random integer generation +and not a random float, it is accurate for any IntegerEvents. So, for instance, on 1000D6, a roll of "1000" +has a 1 in 6**1000 chance of occurring (a 7.059e-777% chance). The roller can accurately model that, so that:: + + table = dt.DiceTable.new().add_die(dt.Die(6), 1000) + roller = Roller(table) + my_roll = roller.roll() + +`my_roll` actually has a chance of being `1000`, although if that actually happened ... er ... +um ... hmmm ... get your computer checked. Seriously! You probably have a higher chance of the Heart Of Gold +actually existing **and** getting picked up by Zaphod Beeblebrox in interstellar space just after escaping +the Vogon destruction of Planet Earth than of rolling `1000`. + +.. module:: dicetables.roller + + +.. autoclass:: Roller + :members: + :undoc-members: + +Here's an example with a custom "random" generator + +>>> import random +>>> import dicetables as dt +>>> class ZeroForever(random.Random): +... def __init__(self, *args, **kwargs): +... self.even_odd_counter = 0 +... super(ZeroForever, self).__init__(*args, **kwargs) +... +... def randrange(self, start, stop=None, step=1, _int=int): +... return 0 +... +>>> my_die = dt.WeightedDie({1: 1, 2: 10**1000}) +>>> roller = dt.Roller(my_die, random_generator=ZeroForever()) +>>> roller.roll_many(10) +[1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + diff --git a/docs/the_basics.rst b/docs/the_basics.rst index 9493d0d..8b7b3b5 100644 --- a/docs/the_basics.rst +++ b/docs/the_basics.rst @@ -111,3 +111,10 @@ StatsStrings(query_values='1,000-1,500', total_occurrences='1.417e+778', one_in_chance='5.809e+365', pct_chance='1.722e-364') + +You can roll events with a :doc:`roller` + +>>> events = dt.DiceTable.new().add_die(dt.Die(6)) +>>> roller = dt.Roller(events) +>>> roller.roll() in [1, 2, 3, 4, 5, 6] +True diff --git a/docs/the_dice.rst b/docs/the_dice.rst index 9058620..3716403 100644 --- a/docs/the_dice.rst +++ b/docs/the_dice.rst @@ -205,6 +205,8 @@ True `Top`_ +.. _Dice-Pools-Section: + Dice Pools ---------- diff --git a/doctests.py b/doctests.py index 45a8ca1..680f971 100644 --- a/doctests.py +++ b/doctests.py @@ -7,13 +7,16 @@ # Note loader and ignore are required arguments for unittest even if unused. def load_tests(loader, tests, ignore): + excluded_dirs = ['venv', '.tox'] + flags = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.IGNORE_EXCEPTION_DETAIL for path, _, files in os.walk('.'): - for file in files: - if file.endswith('.rst'): - name = os.path.join(path, file) - print('added file: ', name) - tests.addTests(doctest.DocFileSuite(name, optionflags=flags)) + if not any(excluded in path for excluded in excluded_dirs): + for file in files: + if file.endswith('.rst'): + name = os.path.join(path, file) + print('added file: ', name) + tests.addTests(doctest.DocFileSuite(name, optionflags=flags)) return tests diff --git a/setup.py b/setup.py index 1c80cf0..8305c2c 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,13 @@ -from setuptools import setup +from setuptools import setup, find_packages def readme(): with open('README.rst') as f: return f.read() + setup(name='dicetables', - version='2.5.0', + version='2.6.0', description='get all combinations for any set of dice', long_description=readme(), keywords='dice, die, statistics, table, probability, combinations', @@ -15,17 +16,17 @@ def readme(): author_email='shaweric01@gmail.com', license='MIT', classifiers=[ - 'Development Status :: 4 - Beta', - "Operating System :: OS Independent", - 'Intended Audience :: Developers', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Games/Entertainment :: Role-Playing', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', + 'Development Status :: 4 - Beta', + "Operating System :: OS Independent", + 'Intended Audience :: Developers', + 'Topic :: Scientific/Engineering :: Mathematics', + 'Topic :: Games/Entertainment :: Role-Playing', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', ], - packages=['dicetables', 'dicetables.tools', 'dicetables.factory', 'dicetables.eventsbases'], + packages=find_packages(exclude=['tests', 'time_trials', 'docs']), install_requires=[], test_suite='nose.collector', tests_require=['nose'], diff --git a/tests/eventsbases/test_eventserrors.py b/tests/eventsbases/test_eventserrors.py index 6ca171f..9b20044 100644 --- a/tests/eventsbases/test_eventserrors.py +++ b/tests/eventsbases/test_eventserrors.py @@ -27,4 +27,4 @@ def test_DiceRecordError_no_message(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/eventsbases/test_integerevents.py b/tests/eventsbases/test_integerevents.py index 7038130..9e77fea 100644 --- a/tests/eventsbases/test_integerevents.py +++ b/tests/eventsbases/test_integerevents.py @@ -4,8 +4,8 @@ import unittest from sys import version_info -from dicetables.eventsbases.integerevents import IntegerEvents, EventsVerifier from dicetables.eventsbases.eventerrors import InvalidEventsError +from dicetables.eventsbases.integerevents import IntegerEvents, EventsVerifier class DummyEvents(IntegerEvents): diff --git a/tests/eventsbases/test_protodie.py b/tests/eventsbases/test_protodie.py index 5c26592..dcaf0fb 100644 --- a/tests/eventsbases/test_protodie.py +++ b/tests/eventsbases/test_protodie.py @@ -184,5 +184,6 @@ def test_ProtoDie_hash(self): hash_str = 'hash of REPR, 2, 3, {4: 5}' self.assertEqual(hash(DummyDie(2, 3, {4: 5}, 'REPR')), hash(hash_str)) + if __name__ == '__main__': unittest.main() diff --git a/tests/factory/test_eventsfactory.py b/tests/factory/test_eventsfactory.py index f5c3805..28fab40 100644 --- a/tests/factory/test_eventsfactory.py +++ b/tests/factory/test_eventsfactory.py @@ -3,15 +3,15 @@ import unittest import warnings -from sys import version_info from itertools import cycle +from sys import version_info from dicetables.additiveevents import AdditiveEvents, EventsDictCreator +from dicetables.dicerecord import DiceRecord from dicetables.dicetable import DiceTable, DetailedDiceTable from dicetables.dieevents import Die -from dicetables.dicerecord import DiceRecord -from dicetables.factory.eventsfactory import EventsFactory, Loader, LoaderError from dicetables.factory.errorhandler import EventsFactoryError +from dicetables.factory.eventsfactory import EventsFactory, Loader, LoaderError from dicetables.factory.warninghandler import EventsFactoryWarning @@ -76,29 +76,34 @@ def assert_no_warning(self, func, *args): def test_assert_EventsFactoryError_contains(self): def raise_events_factory_error(msg): raise EventsFactoryError(msg) + self.assert_EventsFactoryError_contains(('a', 'b', 'c'), raise_events_factory_error, 'abcd') @unittest.expectedFailure def test_assert_EventsFactoryError_contains_fail(self): def raise_events_factory_error(msg): raise EventsFactoryError(msg) + self.assert_EventsFactoryError_contains(('a', 'b', 'e'), raise_events_factory_error, 'abcd') def test_assert_EventsFactoryError_code(self): def raise_events_factory_error(msg): raise EventsFactoryError(msg) + self.assert_EventsFactoryError_code('RAISED', raise_events_factory_error, 'code: RAISED') @unittest.expectedFailure def test_assert_EventsFactoryError_code_wrong_code(self): def raise_events_factory_error(msg): raise EventsFactoryError(msg) + self.assert_EventsFactoryError_code('wrong', raise_events_factory_error, 'code: RAISED') def test_assert_EventsFactoryWarning_code(self): def func(number): warnings.warn('msg\ncode: TEST', EventsFactoryWarning, stacklevel=2) return number + self.assert_EventsFactoryWarning_code(['TEST'], func, 5) def test_assert_EventsFactoryWarning_code_multiple(self): @@ -106,6 +111,7 @@ def func(number): warnings.warn('msg\ncode: TEST', EventsFactoryWarning, stacklevel=2) warnings.warn('other\ncode: TEST2', EventsFactoryWarning, stacklevel=2) return number + self.assert_EventsFactoryWarning_code(['TEST', 'TEST2'], func, 5) @unittest.expectedFailure @@ -121,11 +127,13 @@ def func(number): warnings.warn('code: TEST', EventsFactoryWarning, stacklevel=2) warnings.warn('code: OTHER', Warning, stacklevel=2) return number + self.assert_EventsFactoryWarning_code(['TEST'], func, 5) def test_assert_no_warning_with_no_warning(self): def func(number): return number + self.assert_no_warning(func, 5) @unittest.expectedFailure @@ -133,6 +141,7 @@ def test_assert_no_warning_with_warning(self): def func(number): warnings.warn('msg', EventsFactoryWarning, stacklevel=2) return number + self.assert_no_warning(func, 5) def test_Loader_no_factory_keys_raises_LoaderError(self): @@ -140,11 +149,12 @@ def test_Loader_no_factory_keys_raises_LoaderError(self): def test_Loader_bad_factory_keys_raises_EventsFactoryError(self): class BadByClassPresentButDifferent(object): - factory_keys = ('dice_data', ) + factory_keys = ('dice_data',) + EventsFactory.add_class(BadByClassPresentButDifferent, ('get_dict',)) class BadByBadKeyName(object): - factory_keys = ('so very very wrong', ) + factory_keys = ('so very very wrong',) self.assert_EventsFactoryError_code('CLASS OVERWRITE', Loader(EventsFactory).load, BadByClassPresentButDifferent) @@ -155,12 +165,14 @@ def test_Loader_bad_new_keys_raises_EventsFactoryError(self): class Bob(object): factory_keys = ('dice_data',) new_keys = [('dice_data', 'get_dice', [1])] + self.assert_EventsFactoryError_code('GETTER OVERWRITE', Loader(EventsFactory).load, Bob) def test_Loader_class_already_present(self): class Bob(object): factory_keys = ('dice_data',) - EventsFactory.add_class(Bob, ('dice_data', )) + + EventsFactory.add_class(Bob, ('dice_data',)) self.assertTrue(EventsFactory.has_class(Bob)) self.assertIsNone(Loader(EventsFactory).load(Bob)) self.assertTrue(EventsFactory.has_class(Bob)) @@ -168,6 +180,7 @@ class Bob(object): def test_Loader_no_new_keys(self): class Bob(object): factory_keys = ('dice_data',) + self.assertFalse(EventsFactory.has_class(Bob)) Loader(EventsFactory).load(Bob) self.assertTrue(EventsFactory.has_class(Bob)) @@ -176,6 +189,7 @@ def test_Loader_new_keys(self): class Bob(object): factory_keys = ('my_method', 'my_property') new_keys = [('my_method', 5), ('my_property', 'a', 'property')] + Loader(EventsFactory).load(Bob) self.assertTrue(EventsFactory.get_class_params(Bob), ('my_method', 'my_property')) self.assertEqual(EventsFactory.get_getter_string('my_method'), 'method: "my_method", default: 5') @@ -194,7 +208,7 @@ def test_EventsFactory_has_getter_false(self): self.assertFalse(EventsFactory.has_getter('not_there')) def test_EventsFactory_get_class_params(self): - self.assertEqual(EventsFactory.get_class_params(AdditiveEvents), ('get_dict', )) + self.assertEqual(EventsFactory.get_class_params(AdditiveEvents), ('get_dict',)) def test_EventsFactory_get_class_params_no_class(self): self.assertIsNone(EventsFactory.get_class_params(int)) @@ -209,11 +223,12 @@ def test_EventsFactory_get_getter_string_no_getters(self): def test_EventsFactory_get_keys(self): class X(object): pass + classes = ['AdditiveEvents', 'DetailedDiceTable', 'DiceTable'] getters = ['calc_includes_zeroes', 'dice_data', 'get_dict'] self.assertEqual(EventsFactory.get_keys(), (classes, getters)) - EventsFactory.add_class(X, ('dice_data', )) + EventsFactory.add_class(X, ('dice_data',)) EventsFactory.add_getter('z', 'get', 0) classes.append('X') getters.append('z') @@ -222,6 +237,7 @@ class X(object): def test_EventsFactory_reset(self): class Bob(object): pass + EventsFactory.add_class(Bob, ('get_dict',)) EventsFactory.add_getter('number', 'get_num', 0) self.assertTrue(EventsFactory.has_class(Bob)) @@ -234,6 +250,7 @@ class Bob(object): def test_EventsFactory_add_class(self): class Bob(object): pass + self.assertFalse(EventsFactory.has_class(Bob)) EventsFactory.add_class(Bob, ('get_dict',)) self.assertTrue(EventsFactory.has_class(Bob)) @@ -241,18 +258,19 @@ class Bob(object): def test_EventsFactory_add_class_already_has_class_does_not_change(self): self.assertTrue(EventsFactory.has_class(AdditiveEvents)) - self.assertEqual(EventsFactory.get_class_params(AdditiveEvents), ('get_dict', )) + self.assertEqual(EventsFactory.get_class_params(AdditiveEvents), ('get_dict',)) EventsFactory.add_class(AdditiveEvents, ('get_dict',)) self.assertTrue(EventsFactory.has_class(AdditiveEvents)) self.assertEqual(EventsFactory.get_class_params(AdditiveEvents), ('get_dict',)) def test_EventsFactory_add_class_already_has_class_raises_EventsFactoryError(self): - self.assert_EventsFactoryError_code('CLASS OVERWRITE', EventsFactory.add_class, AdditiveEvents, ('dice_data', )) + self.assert_EventsFactoryError_code('CLASS OVERWRITE', EventsFactory.add_class, AdditiveEvents, ('dice_data',)) def test_EventsFactory_add_class_factory_missing_getter_raises_EventsFactoryError(self): class Bob(object): pass - self.assert_EventsFactoryError_code('MISSING GETTER', EventsFactory.add_class, Bob, ('not_there', )) + + self.assert_EventsFactoryError_code('MISSING GETTER', EventsFactory.add_class, Bob, ('not_there',)) def test_EventsFactory_add_getter_new_method(self): EventsFactory.add_getter('get_num', 0) @@ -291,18 +309,21 @@ def test_EventsFactory_check_raises_warning(self): def test_EventsFactory_check_error(self): class Bob(object): - factory_keys = ('oops', ) + factory_keys = ('oops',) + self.assert_EventsFactoryError_code('MISSING GETTER', EventsFactory.check, Bob) def test_EventsFactory_check_never_runs_loader_if_class_found(self): class Bob(object): - factory_keys = ('will cause error', ) - EventsFactory.add_class(Bob, ('dice_data', )) + factory_keys = ('will cause error',) + + EventsFactory.add_class(Bob, ('dice_data',)) self.assert_no_warning(EventsFactory.check, Bob) def test_EventsFactory_check_only_issues_warning_for_totally_unrelated_class(self): class Bob(object): pass + self.assert_EventsFactoryWarning_code(['CHECK'], EventsFactory.check, Bob) def test_EventFactory_construction__class_in_defaults(self): @@ -314,7 +335,8 @@ def test_EventFactory_construction__class_in_defaults(self): def test_EventFactory_construction__new_class_added_and_called(self): class Bob(AdditiveEvents): pass - EventsFactory.add_class(Bob, ('get_dict', )) + + EventsFactory.add_class(Bob, ('get_dict',)) self.assert_no_warning(EventsFactory.new, Bob) test = EventsFactory.new(Bob) self.assertIs(type(test), Bob) @@ -349,13 +371,14 @@ def test_EventsFactory_construction__new_class_has_no_update_info_init_did_chang def test_EventsFactory_construction__bad_loader_info_ignored_if_class_already_in_factory(self): class Bob(object): - factory_keys = ('whoopsy-doodle', ) - new_keys = [('wrong', ), ('very wrong', )] + factory_keys = ('whoopsy-doodle',) + new_keys = [('wrong',), ('very wrong',)] def __init__(self, num): self.num = num + EventsFactory.add_getter('num', -5, 'property') - EventsFactory.add_class(Bob, ('num', )) + EventsFactory.add_class(Bob, ('num',)) self.assert_no_warning(EventsFactory.new, Bob) test = EventsFactory.new(Bob) self.assertIs(type(test), Bob) @@ -364,40 +387,47 @@ def __init__(self, num): def test_EventsFactory_construction__totally_unrelated_object_with_no_loader_raises_EventsFactoryError(self): class Bob(object): pass + self.assert_EventsFactoryError_code('WTF', EventsFactory.new, Bob) def test_EventsFactory_construction__new_class_bad_factory_keys_raises_EventsFactoryError(self): class BadLoaderKeys(AdditiveEvents): - factory_keys = ('bad and wrong', ) + factory_keys = ('bad and wrong',) + self.assert_EventsFactoryError_code('MISSING GETTER', EventsFactory.new, BadLoaderKeys) def test_EventsFactory_construction__new_class_bad_new_keys_raises_EventsFactoryError(self): class BadLoaderKeys(AdditiveEvents): - factory_keys = ('dice_data', ) + factory_keys = ('dice_data',) new_keys = [('dice_data', 5)] + self.assert_EventsFactoryError_code('GETTER OVERWRITE', EventsFactory.new, BadLoaderKeys) def test_EventsFactory_construction__class_present_wrong_signature_wrong_param_EventsFactoryError(self): class Bob(AdditiveEvents): pass - EventsFactory.add_class(Bob, ('dice_data', )) + + EventsFactory.add_class(Bob, ('dice_data',)) self.assert_EventsFactoryError_code('SIGNATURES DIFFERENT', EventsFactory.new, Bob) def test_EventsFactory_construction__class_present_different_signature_too_many_params_EventsFactoryError(self): class Bob(AdditiveEvents): pass + EventsFactory.add_class(Bob, ('get_dict', 'dice_data')) self.assert_EventsFactoryError_code('SIGNATURES DIFFERENT', EventsFactory.new, Bob) def test_EventsFactory_construction__class_present_different_signature_too_few_params_EventsFactoryError(self): class Bob(DiceTable): pass - EventsFactory.add_class(Bob, ('get_dict', )) + + EventsFactory.add_class(Bob, ('get_dict',)) self.assert_EventsFactoryError_code('SIGNATURES DIFFERENT', EventsFactory.new, Bob) def test_EventsFactory_construction__class_present_wrong_signature_wrong_order_EventsFactoryError(self): class Bob(DiceTable): pass + EventsFactory.add_class(Bob, ('dice_data', 'get_dict')) self.assert_EventsFactoryError_code('SIGNATURES DIFFERENT', EventsFactory.new, Bob) @@ -486,20 +516,21 @@ def test_Events_Factory_warning_correct_message_raised_CHECK(self): will_warn_insert = '' will_warn = 'tests.factory.test_eventsfactory.{}WillWarn'.format(will_warn_insert) expected = ( - 'factory: \n' + - 'Warning code: CHECK\n' + - 'Failed to find/add the following class to the EventsFactory - \n' + - 'class: \n' - '\n' + - 'Warning raised while performing check at instantiation\n' + - '\n' + - 'SOLUTION:\n' + - ' class variable: factory_keys = (getter method/property names)\n' - ' current factory keys are: [\'calc_includes_zeroes\', \'dice_data\', \'get_dict\']\n' + 'factory: \n' + + 'Warning code: CHECK\n' + + 'Failed to find/add the following class to the EventsFactory - \n' + + 'class: \n' + '\n' + + 'Warning raised while performing check at instantiation\n' + + '\n' + + 'SOLUTION:\n' + + ' class variable: factory_keys = (getter method/property names)\n' + ' current factory keys are: [\'calc_includes_zeroes\', \'dice_data\', \'get_dict\']\n' ) class WillWarn(DiceTable): pass + self.assert_EventsFactoryWarning_code([expected.format(will_warn)], EventsFactory.check, WillWarn) def test_Events_Factory_warning_correct_message_CONSTRUCT(self): @@ -509,16 +540,17 @@ def test_Events_Factory_warning_correct_message_CONSTRUCT(self): will_warn = 'tests.factory.test_eventsfactory.{}WillWarn'.format(will_warn_insert) expected = ( - 'factory: \n' + - 'Warning code: CONSTRUCT\n' + - 'Failed to find/add the following class to the EventsFactory - \n' + - 'class: \n' + - '\n' + - 'Class found in factory: \n' + 'factory: \n' + + 'Warning code: CONSTRUCT\n' + + 'Failed to find/add the following class to the EventsFactory - \n' + + 'class: \n' + + '\n' + + 'Class found in factory: \n' ) class WillWarn(DiceTable): pass + self.assert_EventsFactoryWarning_code([expected.format(will_warn), 'CHECK'], EventsFactory.new, WillWarn) def test_Events_Factory_error_correct_message_CLASS_OVERWRITE(self): @@ -537,8 +569,9 @@ def test_Events_Factory_error_correct_message_GETTER_OVERWRITE(self): expected = (factory_name, getter_key, getter_string, new_getter_string) class Bob(object): - factory_keys = ('dice_data', ) + factory_keys = ('dice_data',) new_keys = [('dice_data', [(2, Die(6))], 'method')] + self.assert_EventsFactoryError_contains(expected, EventsFactory.new, Bob) def test_Events_Factory_error_correct_message_MISSING_GETTER(self): @@ -553,6 +586,7 @@ def test_Events_Factory_error_correct_message_MISSING_GETTER(self): class Bob(object): factory_keys = ('dice_data', 'foo') + self.assert_EventsFactoryError_contains(expected, EventsFactory.new, Bob) def test_Events_Factory_error_correct_message_SIGNATURES_DIFFERENT(self): @@ -567,6 +601,7 @@ def test_Events_Factory_error_correct_message_SIGNATURES_DIFFERENT(self): class Bob(object): def __init__(self, num): self.num = num + EventsFactory.add_class(Bob, ('get_dict', 'dice_data')) self.assert_EventsFactoryError_contains(expected, EventsFactory.new, Bob) @@ -581,6 +616,7 @@ def test_Events_Factory_error_correct_message_WTF(self): class Bob(object): pass + self.assert_EventsFactoryError_contains(expected, EventsFactory.new, Bob) def test_BUG_inheritance_issue_RESOLVED_as_best_as_can_be_without_metaclass(self): @@ -593,6 +629,7 @@ def test_BUG_inheritance_issue_RESOLVED_as_best_as_can_be_without_metaclass(self I used StaticDict with EventsFactory so that changes to child dict do not mutate parent dict. """ + class A(EventsFactory): pass @@ -604,7 +641,7 @@ class C(B): self.assertIs(EventsFactory._class_args, C._class_args) self.assertIs(B._getters, A._getters) - B.add_class(int, ('dice_data', )) + B.add_class(int, ('dice_data',)) self.assertEqual(EventsFactory.get_keys()[0], ['AdditiveEvents', 'DetailedDiceTable', 'DiceTable']) self.assertEqual(C.get_keys()[0], ['AdditiveEvents', 'DetailedDiceTable', 'DiceTable', 'int']) EventsFactory.reset() @@ -696,6 +733,7 @@ def test_a_silly_example_to_show_EventsDictCreator_and_EventsFactory_from_params AdditiveEvents({1: 2, 3: 4}).combine(AdditiveEvents({100: 1})).get_dict() {101: 2, 103: 4} """ + class ModifierTable(DiceTable): factory_keys = ('get_dict', 'dice_data', 'modifier') new_keys = [('modifier', 0, 'property')] diff --git a/tests/factory/test_factoryerrorhandler.py b/tests/factory/test_factoryerrorhandler.py index c2a076f..2c1ac3e 100644 --- a/tests/factory/test_factoryerrorhandler.py +++ b/tests/factory/test_factoryerrorhandler.py @@ -3,9 +3,9 @@ import unittest -from dicetables.factory.errorhandler import EventsFactoryError, EventsFactoryErrorHandler from dicetables.dicetable import DiceTable from dicetables.dieevents import Die +from dicetables.factory.errorhandler import EventsFactoryError, EventsFactoryErrorHandler from dicetables.factory.eventsfactory import EventsFactory, Getter @@ -26,35 +26,36 @@ def assert_events_factory_error_message(self, msg, func, *args): def test_assert_events_factory_error_message(self): def my_func(): raise EventsFactoryError('hello') + self.assert_events_factory_error_message('hello', my_func) def test_EventsFactoryErrorHandler_CLASS_OVERWRITE(self): expected = ( - 'Error Code: CLASS OVERWRITE\n' + - 'Factory: \n' + - 'Error At: \n' + - '\n' + - 'Attempted to add class already in factory but used different factory keys.\n' + - 'Class: \n' + - 'Current Factory Keys: (\'get_dict\', \'dice_data\')\n' + - 'Keys Passed In: (\'dice_data\',)\n' + 'Error Code: CLASS OVERWRITE\n' + + 'Factory: \n' + + 'Error At: \n' + + '\n' + + 'Attempted to add class already in factory but used different factory keys.\n' + + 'Class: \n' + + 'Current Factory Keys: (\'get_dict\', \'dice_data\')\n' + + 'Keys Passed In: (\'dice_data\',)\n' ) self.assert_events_factory_error_message(expected, EventsFactoryErrorHandler(EventsFactory).raise_error, 'CLASS OVERWRITE', DiceTable, - ('dice_data', )) + ('dice_data',)) def test_EventsFactoryErrorHandler_GETTER_OVERWRITE(self): expected = ( - 'Error Code: GETTER OVERWRITE\n' + - 'Factory: \n' + - 'Error At: Factory Getter Key: \'dice_data\'\n' + - '\n' + - 'Attempted to add getter key already in factory but used different parameters.\n' + - 'Key: \'dice_data\'\n' + - 'Factory Parameter: method: "dice_data", default: DiceRecord({})\n' + - 'Passed In Parameters: method: "get_dice", default: [(2, Die(6))]\n' + 'Error Code: GETTER OVERWRITE\n' + + 'Factory: \n' + + 'Error At: Factory Getter Key: \'dice_data\'\n' + + '\n' + + 'Attempted to add getter key already in factory but used different parameters.\n' + + 'Key: \'dice_data\'\n' + + 'Factory Parameter: method: "dice_data", default: DiceRecord({})\n' + + 'Passed In Parameters: method: "get_dice", default: [(2, Die(6))]\n' ) self.assert_events_factory_error_message(expected, EventsFactoryErrorHandler(EventsFactory).raise_error, @@ -64,14 +65,14 @@ def test_EventsFactoryErrorHandler_GETTER_OVERWRITE(self): def test_EventsFactoryErrorHandler_MISSING_GETTER(self): expected = ( - 'Error Code: MISSING GETTER\n' + - 'Factory: \n' + - 'Error At: \n' + - '\n' + - 'Attempted to add class with a getter key not in the factory.\n' + - 'Class: \n' + - 'Current Factory Keys: [\'calc_includes_zeroes\', \'dice_data\', \'get_dict\']\n' + - 'Key Passed In: \'foo\'\n' + 'Error Code: MISSING GETTER\n' + + 'Factory: \n' + + 'Error At: \n' + + '\n' + + 'Attempted to add class with a getter key not in the factory.\n' + + 'Class: \n' + + 'Current Factory Keys: [\'calc_includes_zeroes\', \'dice_data\', \'get_dict\']\n' + + 'Key Passed In: \'foo\'\n' ) self.assert_events_factory_error_message(expected, EventsFactoryErrorHandler(EventsFactory).raise_error, @@ -81,14 +82,14 @@ def test_EventsFactoryErrorHandler_MISSING_GETTER(self): def test_EventsFactoryErrorHandler_SIGNATURES_DIFFERENT(self): expected = ( - 'Error Code: SIGNATURES DIFFERENT\n' + - 'Factory: \n' + - 'Error At: \n' + - '\n' + - 'Attempted to construct a class already present in factory, but with a different signature.\n' + - 'Class: \n' + - 'Signature In Factory: (\'get_dict\', \'dice_data\')\n' + - 'To reset the factory to its base state, use EventsFactory.reset()\n' + 'Error Code: SIGNATURES DIFFERENT\n' + + 'Factory: \n' + + 'Error At: \n' + + '\n' + + 'Attempted to construct a class already present in factory, but with a different signature.\n' + + 'Class: \n' + + 'Signature In Factory: (\'get_dict\', \'dice_data\')\n' + + 'To reset the factory to its base state, use EventsFactory.reset()\n' ) self.assert_events_factory_error_message(expected, EventsFactoryErrorHandler(EventsFactory).raise_error, @@ -97,15 +98,15 @@ def test_EventsFactoryErrorHandler_SIGNATURES_DIFFERENT(self): def test_EventsFactoryErrorHandler_WTF(self): expected = ( - 'Error Code: WTF\n' + - 'Factory: \n' + - 'Error At: \n' + - '\n' + - 'Attempted to construct a class unrelated, in any way, to any class in the factory.\n' + - 'EventsFactory can currently construct the following classes:\n' + - '[\'AdditiveEvents\', \'DetailedDiceTable\', \'DiceTable\']\n' + - 'EventsFactory searched the MRO of ,\n' - + 'and found no matches to the classes in the factory.\n' + 'Error Code: WTF\n' + + 'Factory: \n' + + 'Error At: \n' + + '\n' + + 'Attempted to construct a class unrelated, in any way, to any class in the factory.\n' + + 'EventsFactory can currently construct the following classes:\n' + + '[\'AdditiveEvents\', \'DetailedDiceTable\', \'DiceTable\']\n' + + 'EventsFactory searched the MRO of ,\n' + + 'and found no matches to the classes in the factory.\n' ) self.assert_events_factory_error_message(expected, EventsFactoryErrorHandler(EventsFactory).raise_error, diff --git a/tests/factory/test_factorytools.py b/tests/factory/test_factorytools.py index ce1f5a6..55d6c8e 100644 --- a/tests/factory/test_factorytools.py +++ b/tests/factory/test_factorytools.py @@ -2,10 +2,11 @@ from __future__ import absolute_import import unittest -from dicetables.factory.factorytools import StaticDict, Getter -from dicetables.dicetable import DetailedDiceTable + from dicetables.additiveevents import AdditiveEvents from dicetables.dicerecord import DiceRecord +from dicetables.dicetable import DetailedDiceTable +from dicetables.factory.factorytools import StaticDict, Getter class TestFactoryTools(unittest.TestCase): diff --git a/tests/factory/test_factorywarninghandler.py b/tests/factory/test_factorywarninghandler.py index f571b92..85c31e7 100644 --- a/tests/factory/test_factorywarninghandler.py +++ b/tests/factory/test_factorywarninghandler.py @@ -4,10 +4,10 @@ import unittest import warnings -from dicetables.factory.warninghandler import EventsFactoryWarning, EventsFactoryWarningHandler -from dicetables.factory.eventsfactory import EventsFactory from dicetables.additiveevents import AdditiveEvents from dicetables.dicetable import DiceTable +from dicetables.factory.eventsfactory import EventsFactory +from dicetables.factory.warninghandler import EventsFactoryWarning, EventsFactoryWarningHandler class TestEventsFactoryWarningHandler(unittest.TestCase): @@ -28,11 +28,13 @@ def test_assert_warning(self): def func(number): warnings.warn('msg', EventsFactoryWarning, stacklevel=2) return number + self.assert_warning(EventsFactoryWarning, 'msg', func, 5) def test_assert_warning_with_no_warning(self): def func(number): return number + with self.assertRaises(AssertionError) as cm: self.assert_warning(EventsFactoryWarning, 'hi', func, 5) self.assertEqual(cm.exception.args[0], 'Warning not raised or too many warnings') @@ -42,27 +44,28 @@ def func(number): warnings.warn('msg', EventsFactoryWarning, stacklevel=2) warnings.warn('msg', Warning, stacklevel=2) return number + with self.assertRaises(AssertionError) as cm: self.assert_warning(EventsFactoryWarning, 'hi', func, 5) self.assertEqual(cm.exception.args[0], 'Warning not raised or too many warnings') def test_create_message_header(self): expected = ( - '\n' + - 'factory: \n' + - 'Warning code: CHECK\n' + - 'Failed to find/add the following class to the EventsFactory - \n' + - 'class: \n' + '\n' + + 'factory: \n' + + 'Warning code: CHECK\n' + + 'Failed to find/add the following class to the EventsFactory - \n' + + 'class: \n' ) self.assertEqual(create_message_header('CHECK', EventsFactory, EventsFactoryWarningHandler), expected) def test_create_error_code_body_CONSTRUCT(self): expected = ( - '\n' + - 'Class found in factory: \n' + - 'attempted object construction using its signature. tried to return instance of original class.\n' + - 'If that had failed, returned instance of the class found in EventsFactory.\n' + - '\n' + '\n' + + 'Class found in factory: \n' + + 'attempted object construction using its signature. tried to return instance of original class.\n' + + 'If that had failed, returned instance of the class found in EventsFactory.\n' + + '\n' ) self.assertEqual(create_error_code_body((EventsFactoryWarningHandler, AdditiveEvents), 'CONSTRUCT'), expected) @@ -75,19 +78,19 @@ def test_create_error_code_body_CHECK(self): def test_create_instructions(self): self.maxDiff = None expected = ( - 'SOLUTION:\n' + - ' class variable: factory_keys = (getter method/property names)\n' - ' current factory keys are: [\'calc_includes_zeroes\', \'dice_data\', \'get_dict\']\n' + - ' class variable: new_keys = [(info for each key not already in factory)]\n' + - ' Each tuple in "new_keys" is (getter_name, default_value, "property"/"method")\n' + - 'ex:\n' + - ' NewClass(Something):\n' + - ' factory_keys = ("dice_data", "get_dict", "get_thingy", "label")\n' + - ' new_keys = [("get_thingy", 0, "method"),\n' + - ' ("label", "", "property")]\n' + - '\n' + - ' def __init__(self, events_dict, dice_list, new_thingy, label):\n' + - ' ....\n' + 'SOLUTION:\n' + + ' class variable: factory_keys = (getter method/property names)\n' + ' current factory keys are: [\'calc_includes_zeroes\', \'dice_data\', \'get_dict\']\n' + + ' class variable: new_keys = [(info for each key not already in factory)]\n' + + ' Each tuple in "new_keys" is (getter_name, default_value, "property"/"method")\n' + + 'ex:\n' + + ' NewClass(Something):\n' + + ' factory_keys = ("dice_data", "get_dict", "get_thingy", "label")\n' + + ' new_keys = [("get_thingy", 0, "method"),\n' + + ' ("label", "", "property")]\n' + + '\n' + + ' def __init__(self, events_dict, dice_list, new_thingy, label):\n' + + ' ....\n' ) self.assertEqual(create_instructions(EventsFactory), expected) @@ -126,10 +129,10 @@ def create_warning_message(factory, code, *classes): def create_message_header(code, factory, failed_class): intro = ( - '\nfactory: {}\n'.format(factory) + - 'Warning code: {}\n'.format(code) + - 'Failed to find/add the following class to the EventsFactory - \n' + - 'class: {}\n'.format(failed_class) + '\nfactory: {}\n'.format(factory) + + 'Warning code: {}\n'.format(code) + + 'Failed to find/add the following class to the EventsFactory - \n' + + 'class: {}\n'.format(failed_class) ) return intro @@ -138,9 +141,9 @@ def create_error_code_body(classes, code): if code == 'CONSTRUCT': in_factory = classes[1] middle = ( - '\nClass found in factory: {}\n'.format(in_factory) + - 'attempted object construction using its signature. tried to return instance of original class.\n' + - 'If that had failed, returned instance of the class found in EventsFactory.\n\n' + '\nClass found in factory: {}\n'.format(in_factory) + + 'attempted object construction using its signature. tried to return instance of original class.\n' + + 'If that had failed, returned instance of the class found in EventsFactory.\n\n' ) else: middle = '\nWarning raised while performing check at instantiation\n\n' @@ -149,18 +152,18 @@ def create_error_code_body(classes, code): def create_instructions(factory): instructions = ( - 'SOLUTION:\n' + - ' class variable: factory_keys = (getter method/property names)\n' - ' current factory keys are: {}\n'.format(factory.get_keys()[1]) + - ' class variable: new_keys = [(info for each key not already in factory)]\n' + - ' Each tuple in "new_keys" is (getter_name, default_value, "property"/"method")\n' - 'ex:\n' + - ' NewClass(Something):\n' + - ' factory_keys = ("dice_data", "get_dict", "get_thingy", "label")\n' + - ' new_keys = [("get_thingy", 0, "method"),\n' + - ' ("label", "", "property")]\n\n' + - ' def __init__(self, events_dict, dice_list, new_thingy, label):\n' + - ' ....\n' + 'SOLUTION:\n' + + ' class variable: factory_keys = (getter method/property names)\n' + ' current factory keys are: {}\n'.format(factory.get_keys()[1]) + + ' class variable: new_keys = [(info for each key not already in factory)]\n' + + ' Each tuple in "new_keys" is (getter_name, default_value, "property"/"method")\n' + 'ex:\n' + + ' NewClass(Something):\n' + + ' factory_keys = ("dice_data", "get_dict", "get_thingy", "label")\n' + + ' new_keys = [("get_thingy", 0, "method"),\n' + + ' ("label", "", "property")]\n\n' + + ' def __init__(self, events_dict, dice_list, new_thingy, label):\n' + + ' ....\n' ) return instructions diff --git a/tests/test_bestworstmid.py b/tests/test_bestworstmid.py index 5f06254..31c591f 100644 --- a/tests/test_bestworstmid.py +++ b/tests/test_bestworstmid.py @@ -1,10 +1,9 @@ import unittest -from dicetables.tools.orderedcombinations import ordered_combinations_of_events +from dicetables import Die, WeightedDie, ModDie from dicetables.bestworstmid import (DicePool, BestOfDicePool, WorstOfDicePool, UpperMidOfDicePool, LowerMidOfDicePool, generate_events_dict) - -from dicetables import Die, WeightedDie, ModDie +from dicetables.tools.orderedcombinations import ordered_combinations_of_events class MockOfDicePool(DicePool): @@ -80,9 +79,9 @@ def test_DicePool_weight_info(self): test = MockOfDicePool(WeightedDie({1: 2, 2: 4}), 5, 2) expected = ( - 'Mock 2 of 5D2 W:6\ninput_die info:\n' + - ' a roll of 1 has a weight of 2\n' + - ' a roll of 2 has a weight of 4' + 'Mock 2 of 5D2 W:6\ninput_die info:\n' + + ' a roll of 1 has a weight of 2\n' + + ' a roll of 2 has a weight of 4' ) self.assertEqual(test.weight_info(), expected) @@ -121,10 +120,11 @@ def test_BestOfDicePool_get_dict_ModDie(self): def test_BestOfDicePool_get_dict_WeightedDie(self): test = BestOfDicePool(WeightedDie({1: 2, 3: 3}), 3, 2) - self.assertEqual(test.get_dict(), {2: 1*(2*2*2), 4: 3*(2*2*3), 6: 3*(2*3*3)+1*(3*3*3)}) + self.assertEqual(test.get_dict(), + {2: 1 * (2 * 2 * 2), 4: 3 * (2 * 2 * 3), 6: 3 * (2 * 3 * 3) + 1 * (3 * 3 * 3)}) test = BestOfDicePool(WeightedDie({1: 2, 3: 3}), 3, 1) - self.assertEqual(test.get_dict(), {1: 8, 3: 3*(2*2*3)+3*(2*3*3)+1*(3*3*3)}) + self.assertEqual(test.get_dict(), {1: 8, 3: 3 * (2 * 2 * 3) + 3 * (2 * 3 * 3) + 1 * (3 * 3 * 3)}) def test_BestOfDicePool_str(self): self.assertEqual(str(BestOfDicePool(Die(2), 2, 1)), 'Best 1 of 2D2') @@ -162,10 +162,10 @@ def test_WorstOfDicePool_get_dict_ModDie(self): def test_WorstOfDicePool_get_dict_WeightedDie(self): test = WorstOfDicePool(WeightedDie({1: 2, 3: 3}), 3, 2) - self.assertEqual(test.get_dict(), {2: (2*2*2)+3*(2*2*3), 4: 3*(2*3*3), 6: 1*(3*3*3)}) + self.assertEqual(test.get_dict(), {2: (2 * 2 * 2) + 3 * (2 * 2 * 3), 4: 3 * (2 * 3 * 3), 6: 1 * (3 * 3 * 3)}) test = WorstOfDicePool(WeightedDie({1: 2, 3: 3}), 3, 1) - self.assertEqual(test.get_dict(), {1: 8+3*(2*2*3)+3*(2*3*3), 3: 1*(3*3*3)}) + self.assertEqual(test.get_dict(), {1: 8 + 3 * (2 * 2 * 3) + 3 * (2 * 3 * 3), 3: 1 * (3 * 3 * 3)}) def test_WorstOfDicePool_str(self): self.assertEqual(str(WorstOfDicePool(Die(2), 2, 1)), 'Worst 1 of 2D2') @@ -239,10 +239,10 @@ def test_UpperMidOfDicePool_get_dict_WeightedDie(self): (3, 3, 3): 27} """ test = UpperMidOfDicePool(WeightedDie({1: 2, 3: 3}), 3, 2) - self.assertEqual(test.get_dict(), {2: 8, 4: 36, 6: 54+27}) + self.assertEqual(test.get_dict(), {2: 8, 4: 36, 6: 54 + 27}) test = UpperMidOfDicePool(WeightedDie({1: 2, 3: 3}), 3, 1) - self.assertEqual(test.get_dict(), {1: 44, 3: 54+27}) + self.assertEqual(test.get_dict(), {1: 44, 3: 54 + 27}) def test_UpperMidOfDicePool_str(self): self.assertEqual(str(UpperMidOfDicePool(Die(2), 2, 1)), 'UpperMid 1 of 2D2') @@ -317,10 +317,10 @@ def test_LowerMidOfDicePool_get_dict_WeightedDie(self): (3, 3, 3): 27} """ test = LowerMidOfDicePool(WeightedDie({1: 2, 3: 3}), 3, 2) - self.assertEqual(test.get_dict(), {2: 8+36, 4: 54, 6: 27}) + self.assertEqual(test.get_dict(), {2: 8 + 36, 4: 54, 6: 27}) test = LowerMidOfDicePool(WeightedDie({1: 2, 3: 3}), 3, 1) - self.assertEqual(test.get_dict(), {1: 44, 3: 54+27}) + self.assertEqual(test.get_dict(), {1: 44, 3: 54 + 27}) def test_LowerMidOfDicePool_str(self): self.assertEqual(str(LowerMidOfDicePool(Die(2), 2, 1)), 'LowerMid 1 of 2D2') diff --git a/tests/test_dicerecord.py b/tests/test_dicerecord.py index b9b1ab4..db6e22f 100644 --- a/tests/test_dicerecord.py +++ b/tests/test_dicerecord.py @@ -175,5 +175,6 @@ def test_DiceRecord__repr__(self): possible_reprs = ('DiceRecord({Die(2): 2, Die(3): 5})', 'DiceRecord({Die(3): 5, Die(2): 2})') self.assertIn(repr(record), possible_reprs) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_dicetable.py b/tests/test_dicetable.py index 386ee38..2a169e6 100644 --- a/tests/test_dicetable.py +++ b/tests/test_dicetable.py @@ -3,10 +3,10 @@ import unittest +from dicetables.dicerecord import DiceRecord from dicetables.dicetable import DiceTable, DetailedDiceTable from dicetables.dieevents import Die, ModWeightedDie, ModDie, StrongDie, Modifier from dicetables.eventsbases.eventerrors import InvalidEventsError, DiceRecordError -from dicetables.dicerecord import DiceRecord class TestDiceTable(unittest.TestCase): @@ -280,7 +280,7 @@ def test_DetailedDiceTable_action_keeps_include_zeroes_info_false(self): table = DetailedDiceTable({1: 1}, DiceRecord.new(), calc_includes_zeroes=False) new = table.add_die(Die(2), 1) self.assertFalse(new.calc.include_zeroes) - + def test_DetailedDiceTable_info_property(self): table = DetailedDiceTable({1: 1}, DiceRecord.new(), calc_includes_zeroes=False) self.assertEqual(table.info.all_events(), [(1, 1)]) @@ -408,5 +408,6 @@ def test_DetailedDiceTable_repr_with_dice(self): self.assertEqual(repr(table), '') + if __name__ == '__main__': unittest.main() diff --git a/tests/test_dieevents.py b/tests/test_dieevents.py index a0c43dd..66869a7 100644 --- a/tests/test_dieevents.py +++ b/tests/test_dieevents.py @@ -168,7 +168,7 @@ def test_WeightedDie_get_size_edge_case(self): self.assertEqual(WeightedDie({1: 1, 3: 0}).get_size(), 3) def test_WeightedDie_get_weight(self): - self.assertEqual(WeightedDie({1: 2, 3: 5}).get_weight(), 2+5) + self.assertEqual(WeightedDie({1: 2, 3: 5}).get_weight(), 2 + 5) def test_WeightedDie_get_dict(self): self.assertEqual(WeightedDie({1: 2, 3: 4}).get_dict(), {1: 2, 3: 4}) @@ -522,7 +522,7 @@ def test_ExplodingOn_negative_explosions(self): self.assertRaises(ValueError, ExplodingOn, Die(3), (1,), -1) def test_ExplodingOn_get_dict_one_explodes_on_value(self): - die = ExplodingOn(Die(3), (2, )) + die = ExplodingOn(Die(3), (2,)) self.assertEqual(die.get_dict(), {1: 9, 3: 12, 5: 4, 6: 1, 7: 1}) def test_ExplodingOn_get_dict_regular_die_different_numbers(self): @@ -576,13 +576,13 @@ def test_ExplodingOn_get_dict_WeightedDie_set_explosions(self): die = ExplodingOn(WeightedDie({1: 1, 2: 2, 3: 3}), (2, 3), explosions=3) level_zero = {1: 216} - level_one = [{3: 36*2}, {4: 36*3}] # 2 + roll / 3 + roll + level_one = [{3: 36 * 2}, {4: 36 * 3}] # 2 + roll / 3 + roll level_two = [{5: 4 * 6}, # 2 + 2 + roll {6: 6 * 6}, # 2 + 3 + roll {6: 6 * 6}, # 3 + 2 + roll {7: 9 * 6}] # 3 + 3 + roll - level_three = [{7: 8, 8: 8*2, 9: 8*3}, # 2 + 2 + 2 + roll - {8: 12, 9: 12*2, 10: 12*3}, # 2 + 2 + 3 + roll + level_three = [{7: 8, 8: 8 * 2, 9: 8 * 3}, # 2 + 2 + 2 + roll + {8: 12, 9: 12 * 2, 10: 12 * 3}, # 2 + 2 + 3 + roll {8: 12, 9: 12 * 2, 10: 12 * 3}, # 2 + 3 + 2 + roll {9: 18, 10: 18 * 2, 11: 18 * 3}, # 2 + 3 + 3 + roll {8: 12, 9: 12 * 2, 10: 12 * 3}, # 3 + 2 + 2 + roll @@ -593,8 +593,8 @@ def test_ExplodingOn_get_dict_WeightedDie_set_explosions(self): for sub_dict in level_one + level_two + level_three: answer = add_dicts(answer, sub_dict) - self.assertEqual(answer, {1: 216, 3: 72, 4: 108, 5: 24, 6: 72, 7: 62, 8: 16 + 12*3, 9: 24*4+18*3, - 10: 36*6+27, 11: 162+54, 12: 81}) + self.assertEqual(answer, {1: 216, 3: 72, 4: 108, 5: 24, 6: 72, 7: 62, 8: 16 + 12 * 3, 9: 24 * 4 + 18 * 3, + 10: 36 * 6 + 27, 11: 162 + 54, 12: 81}) self.assertEqual(answer, die.get_dict()) def test_ExplodingOn_get_dict_edge_case_empty_explodes_on(self): @@ -618,7 +618,7 @@ def test_ExplodingOn_get_dict_edge_case_explodes_on_duplicate_values(self): def test_ExplodingOn_get_size(self): dice = [Die(3), ModDie(3, 1), WeightedDie({1: 2, 2: 4}), ModWeightedDie({1: 2, 3: 4}, -1), StrongDie(Die(4), 2)] for die in dice: - self.assertEqual(ExplodingOn(die, (2, ), 3).get_size(), die.get_size()) + self.assertEqual(ExplodingOn(die, (2,), 3).get_size(), die.get_size()) def test_ExplodingOn_get_weight_changes_according_to_len_explodes_on(self): weight_zero = Die(6) @@ -626,22 +626,22 @@ def test_ExplodingOn_get_weight_changes_according_to_len_explodes_on(self): self.assertEqual(weight_zero.get_weight(), 0) self.assertEqual(weight_six.get_weight(), 6) - self.assertEqual(ExplodingOn(weight_zero, (1, )).get_weight(), 1) + self.assertEqual(ExplodingOn(weight_zero, (1,)).get_weight(), 1) self.assertEqual(ExplodingOn(weight_zero, (1, 2)).get_weight(), 2) self.assertEqual(ExplodingOn(weight_zero, (1, 3, 5)).get_weight(), 3) - self.assertEqual(ExplodingOn(weight_six, (1, )).get_weight(), 7) + self.assertEqual(ExplodingOn(weight_six, (1,)).get_weight(), 7) self.assertEqual(ExplodingOn(weight_six, (1, 2)).get_weight(), 8) self.assertEqual(ExplodingOn(weight_six, (1, 3, 5)).get_weight(), 9) def test_ExplodingOn_get_input_die(self): dice = [Die(3), ModDie(3, 1), WeightedDie({1: 2, 2: 4}), ModWeightedDie({1: 2, 3: 4}, -1), StrongDie(Die(4), 2)] for die in dice: - self.assertEqual(ExplodingOn(die, (2, ), 3).get_input_die(), die) + self.assertEqual(ExplodingOn(die, (2,), 3).get_input_die(), die) def test_ExplodingOn_get_explosions(self): - two = ExplodingOn(Modifier(1), (1, )) - five = ExplodingOn(Modifier(1), (1, ), 5) + two = ExplodingOn(Modifier(1), (1,)) + five = ExplodingOn(Modifier(1), (1,), 5) self.assertEqual(two.get_explosions(), 2) self.assertEqual(five.get_explosions(), 5) @@ -690,5 +690,4 @@ def test_ExplodingOn_all_relevant_methods_ignore_duplicate_explodes_on_values(se if __name__ == '__main__': - unittest.main() diff --git a/tests/test_eventsinfo.py b/tests/test_eventsinfo.py index 1beec94..94744f0 100644 --- a/tests/test_eventsinfo.py +++ b/tests/test_eventsinfo.py @@ -3,8 +3,9 @@ from __future__ import absolute_import import unittest -from dicetables import AdditiveEvents + import dicetables.eventsinfo as ti +from dicetables import AdditiveEvents class TestEventsInfo(unittest.TestCase): @@ -201,22 +202,22 @@ def test_EventsCalculations_full_table_string_edge_case(self): self.assertEqual(calculator.full_table_string(), '0: 1\n') def test_EventsCalculations_full_table_string_uses_NumberFormatter_on_occurrences_only(self): - events = AdditiveEvents({10000: 10**1000}) + events = AdditiveEvents({10000: 10 ** 1000}) calculator = ti.EventsCalculations(events) self.assertEqual(calculator.full_table_string(), '10000: 1.000e+1000\n') def test_EventsCalculations_full_table_string_shown_digits_lt_one(self): - events = AdditiveEvents({10000: 10**1000}) + events = AdditiveEvents({10000: 10 ** 1000}) calculator = ti.EventsCalculations(events) self.assertEqual(calculator.full_table_string(shown_digits=-5), '10000: 1e+1000\n') def test_EventsCalculations_full_table_string_shown_digits_lt_four(self): - events = AdditiveEvents({10000: 10**1000}) + events = AdditiveEvents({10000: 10 ** 1000}) calculator = ti.EventsCalculations(events) self.assertEqual(calculator.full_table_string(shown_digits=2), '10000: 1.0e+1000\n') def test_EventsCalculations_full_table_string_show_digits_gt_four(self): - events = AdditiveEvents({10000: 10**1000}) + events = AdditiveEvents({10000: 10 ** 1000}) calculator = ti.EventsCalculations(events) self.assertEqual(calculator.full_table_string(shown_digits=6), '10000: 1.00000e+1000\n') @@ -245,7 +246,7 @@ def test_EventsCalculations_full_table_string_max_power_for_commaed_neg_many(sel self.assertEqual(calculator.full_table_string(max_power_for_commaed=-10), expected) def test_EventsCalculations_full_table_string_max_power_for_commaed_high(self): - events = AdditiveEvents({1: 10**10, 2: 10**11}) + events = AdditiveEvents({1: 10 ** 10, 2: 10 ** 11}) calculator = ti.EventsCalculations(events) expected = '1: 10,000,000,000\n2: 1.000e+11\n' self.assertEqual(calculator.full_table_string(max_power_for_commaed=10), expected) @@ -257,16 +258,16 @@ def test_get_fast_pct_number_small(self): self.assertEqual(ti.get_fast_pct_number(10, 100), 10.0) def test_get_fast_pct_number_small_denominator_big_numerator(self): - self.assertEqual(ti.get_fast_pct_number(10, 10**500), 0) + self.assertEqual(ti.get_fast_pct_number(10, 10 ** 500), 0) def test_get_fast_pct_number_big_denominator_big_numerator(self): - self.assertEqual(ti.get_fast_pct_number(10**499, 10**500), 10.0) + self.assertEqual(ti.get_fast_pct_number(10 ** 499, 10 ** 500), 10.0) def test_get_fast_pct_still_pretty_good(self): - self.assertAlmostEqual(ti.get_fast_pct_number(4, 7), 400./7., places=10) - self.assertAlmostEqual(ti.get_fast_pct_number(4*10**500, 7*10**500), 400./7., places=10) - self.assertNotAlmostEqual(ti.get_fast_pct_number(4, 7), 400./7., places=15) - self.assertNotAlmostEqual(ti.get_fast_pct_number(4*10**500, 7*10**500), 400./7., places=15) + self.assertAlmostEqual(ti.get_fast_pct_number(4, 7), 400. / 7., places=10) + self.assertAlmostEqual(ti.get_fast_pct_number(4 * 10 ** 500, 7 * 10 ** 500), 400. / 7., places=10) + self.assertNotAlmostEqual(ti.get_fast_pct_number(4, 7), 400. / 7., places=15) + self.assertNotAlmostEqual(ti.get_fast_pct_number(4 * 10 ** 500, 7 * 10 ** 500), 400. / 7., places=15) def test_get_exact_pct_number_zero(self): self.assertEqual(ti.get_exact_pct_number(0, 100), 0) @@ -275,14 +276,14 @@ def test_get_exact_pct_number_small(self): self.assertEqual(ti.get_exact_pct_number(10, 100), 10.0) def test_get_exact_pct_number_small_denominator_big_numerator(self): - self.assertEqual(ti.get_exact_pct_number(10, 10**500), 0) + self.assertEqual(ti.get_exact_pct_number(10, 10 ** 500), 0) def test_get_exact_pct_number_big_denominator_big_numerator(self): - self.assertEqual(ti.get_exact_pct_number(10**499, 10**500), 10.0) + self.assertEqual(ti.get_exact_pct_number(10 ** 499, 10 ** 500), 10.0) def test_get_exact_pct_is_exact(self): - self.assertEqual(ti.get_exact_pct_number(4, 7), 400./7.) - self.assertEqual(ti.get_exact_pct_number(4*10**500, 7*10**500), 400./7.) + self.assertEqual(ti.get_exact_pct_number(4, 7), 400. / 7.) + self.assertEqual(ti.get_exact_pct_number(4 * 10 ** 500, 7 * 10 ** 500), 400. / 7.) def test_EventsCalculations_percentage_points(self): events = AdditiveEvents({1: 1, 3: 3}) @@ -298,8 +299,8 @@ def test_EventsCalculations_percentage_points_is_not_exact(self): events = AdditiveEvents({1: 3, 3: 4}) calculator = ti.EventsCalculations(events, False) three_sevenths, four_sevenths = calculator.percentage_points() - self.assertAlmostEqual(three_sevenths[1], 300./7., places=10) - self.assertAlmostEqual(four_sevenths[1], 400./7., places=10) + self.assertAlmostEqual(three_sevenths[1], 300. / 7., places=10) + self.assertAlmostEqual(four_sevenths[1], 400. / 7., places=10) def test_EventsCalculations_percentage_axes(self): events = AdditiveEvents({1: 1, 3: 3}) @@ -315,8 +316,8 @@ def test_EventsCalculations_percentage_axes_is_not_exact(self): events = AdditiveEvents({1: 3, 3: 4}) calculator = ti.EventsCalculations(events, False) three_sevenths, four_sevenths = calculator.percentage_axes()[1] - self.assertAlmostEqual(three_sevenths, 300./7., places=10) - self.assertAlmostEqual(four_sevenths, 400./7., places=10) + self.assertAlmostEqual(three_sevenths, 300. / 7., places=10) + self.assertAlmostEqual(four_sevenths, 400. / 7., places=10) def test_EventsCalculations_percentage_points_exact(self): events = AdditiveEvents({1: 1, 3: 3}) @@ -332,8 +333,8 @@ def test_EventsCalculations_percentage_points_exact_is_exact(self): events = AdditiveEvents({1: 3, 3: 4}) calculator = ti.EventsCalculations(events, False) three_sevenths, four_sevenths = calculator.percentage_points_exact() - self.assertAlmostEqual(three_sevenths[1], 300./7., places=10) - self.assertAlmostEqual(four_sevenths[1], 400./7., places=10) + self.assertAlmostEqual(three_sevenths[1], 300. / 7., places=10) + self.assertAlmostEqual(four_sevenths[1], 400. / 7., places=10) def test_EventsCalculations_percentage_axes_exact(self): events = AdditiveEvents({1: 1, 3: 3}) @@ -349,8 +350,8 @@ def test_EventsCalculations_percentage_axes_exact_is_exact(self): events = AdditiveEvents({1: 3, 3: 4}) calculator = ti.EventsCalculations(events, False) three_sevenths, four_sevenths = calculator.percentage_axes_exact()[1] - self.assertEqual(three_sevenths, 300./7.) - self.assertEqual(four_sevenths, 400./7.) + self.assertEqual(three_sevenths, 300. / 7.) + self.assertEqual(four_sevenths, 400. / 7.) def test_EventsCalculations_log10_points_include_zeroes_is_false(self): events = AdditiveEvents({1: 10, 3: 100}) @@ -408,7 +409,7 @@ def test_EventsCalculations_stats_strings_pct_less_than_zero(self): self.assertEqual(calculator.stats_strings([1]), expected) def test_EventsCalculations_stats_strings_pct_much_less_than_zero(self): - calculator = ti.EventsCalculations(AdditiveEvents({1: 1, 2: 10**2000})) + calculator = ti.EventsCalculations(AdditiveEvents({1: 1, 2: 10 ** 2000})) expected = ('1', '1', '1.000e+2000', '1.000e+2000', '1.000e-1998') self.assertEqual(calculator.stats_strings([1]), expected) @@ -486,7 +487,6 @@ def test_EventsCalculations_stats_strings_min_power_for_fixed_pt_positive_number self.assertEqual(calculator.stats_strings([1], min_power_for_fixed_pt=10), expected) def test_EventsCalculations_stats_strings_named_tuple_values(self): - calculator = ti.EventsCalculations(AdditiveEvents({1: 2, 2: 10 ** 2000})) test_result = calculator.stats_strings([1]) query_values = '1' @@ -513,6 +513,7 @@ def test_EventsCalculations_stats_strings_named_tuple_values(self): note: the following are wrapper functions. These tests simply confirm that the presets work. For full test see above and (for format_number) test_numberformatter.py """ + def test_events_range(self): events = AdditiveEvents({1: 1, 2: 3, 5: 7}) self.assertEqual(ti.events_range(events), (1, 5)) diff --git a/tests/test_parser.py b/tests/test_parser.py index e7a6406..2b0cfdf 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,15 +1,14 @@ # pylint: disable=missing-docstring, invalid-name, too-many-public-methods from __future__ import absolute_import -import unittest import ast import sys +import unittest -from dicetables.dieevents import Die, ModDie, WeightedDie, ModWeightedDie, StrongDie, Modifier, Exploding, ExplodingOn from dicetables.bestworstmid import BestOfDicePool, WorstOfDicePool, UpperMidOfDicePool, LowerMidOfDicePool -from dicetables.tools.orderedcombinations import count_unique_combination_keys - +from dicetables.dieevents import Die, ModDie, WeightedDie, ModWeightedDie, StrongDie, Modifier, Exploding, ExplodingOn from dicetables.parser import Parser, ParseError, LimitsError, make_int, make_int_dict, make_int_tuple +from dicetables.tools.orderedcombinations import count_unique_combination_keys class TestParser(unittest.TestCase): @@ -134,8 +133,8 @@ def __init__(self, name, size): parser.make_die(die_node) self.assertEqual( cm.exception.args[0], ( - "Failed to create die: with param types: ('string', 'int'). " + - "One or more param types not recognized." + "Failed to create die: with param types: ('string', 'int'). " + + "One or more param types not recognized." ) ) @@ -440,6 +439,7 @@ def __init__(self, funky_new_die_size): def test_parse_within_limits_white_box_test_non_hardcoded_kwargs_for_dicepool(self): """This could be run as a regular blackbox test, but it would take 0.5s for this single test.""" + class IgnoresLimits(BestOfDicePool): def __init__(self, a_die, pool_size, select): super(IgnoresLimits, self).__init__(a_die, pool_size, select) diff --git a/tests/test_roller.py b/tests/test_roller.py new file mode 100644 index 0000000..685fe38 --- /dev/null +++ b/tests/test_roller.py @@ -0,0 +1,178 @@ +import random +import unittest +from sys import version + +from dicetables.additiveevents import AdditiveEvents +from dicetables.roller import Roller + +VERSION = int(version[0]) + + +class TestRoller(unittest.TestCase): + def assert_alias_table_has_expected_counts(self, alias_table, expected_counts): + actual_counts = dict.fromkeys(expected_counts.keys(), 0) + for length_val in range(alias_table.length): + for height_val in range(alias_table.height): + actual_counts[alias_table.get(length_val, height_val)] += 1 + + self.assertEqual(expected_counts, actual_counts) + + actual_primaries = {alias.primary for alias in alias_table.to_list()} + expected_primaries = set(expected_counts.keys()) + self.assertEqual(actual_primaries, expected_primaries) + + def test_roller_init(self): + events = AdditiveEvents({1: 2, 3: 4, 5: 6}) + roller = Roller(events) + alias_table = roller.alias_table + # see tools/test_alias_table.py + self.assertEqual(alias_table.height, 2 + 4 + 6) + self.assertEqual(alias_table.length, 3) + self.assert_alias_table_has_expected_counts(alias_table, {1: 6, 3: 12, 5: 18}) + + def test_roller_init_default_random_instance(self): + events = AdditiveEvents.new() + roller = Roller(events) + self.assertEqual(roller.random_generator, random) + + def test_roller_init_new_random_instance(self): + events = AdditiveEvents.new() + + class MyRandom(random.Random): + pass + + my_random = MyRandom() + roller = Roller(events, my_random) + self.assertEqual(roller.random_generator, my_random) + + def test_roller_roll_only_one_roll(self): + events = AdditiveEvents.new() + roller = Roller(events) + for _ in range(10): + self.assertEqual(roller.roll(), 0) + + def test_roller_with_distribution(self): + random.seed(1346) + events = AdditiveEvents({1: 1, 2: 2, 3: 1}) + roller = Roller(events) + + times = 100 + rolls = [roller.roll() for _ in range(times)] + ones = rolls.count(1) + twos = rolls.count(2) + threes = rolls.count(3) + expected = [21, 50, 29] + if VERSION < 3: + expected = [30, 45, 25] + self.assertEqual([ones, twos, threes], expected) + self.assertEqual(sum(expected), times) + + def test_roller_with_custom_generator(self): + class MyRandom(random.Random): + def __init__(self, range_answer, *args, **kwargs): + self.range_answer = range_answer + super(MyRandom, self).__init__(*args, **kwargs) + + def randrange(self, start, stop=None, step=1, _int=int): + return self.range_answer % start + + events = AdditiveEvents({1: 1, 2: 2}) + random_generator = MyRandom(1) + + roller = Roller(events, random_generator) + self.assertEqual(roller.alias_table.get(1, 1), 2) + + for _ in range(10): + self.assertEqual(roller.roll(), 2) + + random_generator = MyRandom(0) + roller = Roller(events, random_generator) + self.assertEqual(roller.alias_table.get(0, 0), 1) + for _ in range(10): + self.assertEqual(roller.roll(), 1) + + def test_roll_many_with_distribution(self): + random.seed(9876541) + times = 100 + + events = AdditiveEvents({1: 1, 2: 2, 3: 1}) + roller = Roller(events) + result = roller.roll_many(times) + ones = result.count(1) + twos = result.count(2) + threes = result.count(3) + expected = [25, 54, 21] + if VERSION < 3: + expected = [27, 46, 27] + self.assertEqual([ones, twos, threes], expected) + self.assertEqual(sum(expected), times) + + def test_roll_many_with_custom_generator(self): + class MyRandom(random.Random): + def __init__(self, range_answer, *args, **kwargs): + self.range_answer = range_answer + super(MyRandom, self).__init__(*args, **kwargs) + + def randrange(self, start, stop=None, step=1, _int=int): + return self.range_answer % start + + events = AdditiveEvents({1: 1, 2: 2}) + random_generator = MyRandom(1) + + roller = Roller(events, random_generator) + self.assertEqual(roller.alias_table.get(1, 1), 2) + self.assertEqual([2] * 10, roller.roll_many(10)) + + random_generator = MyRandom(0) + roller = Roller(events, random_generator) + self.assertEqual(roller.alias_table.get(0, 0), 1) + self.assertEqual([1] * 10, roller.roll_many(10)) + + def test_roll_with_large_numbers(self): + random.seed(34889970) + events = AdditiveEvents({1: 1, 2: 10 ** 1000, 3: 2 * 10 ** 1000}) + roller = Roller(events) + + times = 100 + result = [roller.roll() for _ in range(times)] + ones = result.count(1) + twos = result.count(2) + threes = result.count(3) + expected = [0, 29, 71] + if VERSION < 3: + expected = [0, 31, 69] + self.assertEqual([ones, twos, threes], expected) + self.assertEqual(sum(expected), times) + + def test_roll_with_large_numbers_still_has_remote_possibility_to_roll_small_number(self): + class MyRandom(random.Random): + def __init__(self, large_range_value, *args, **kwargs): + self.large_range_value = large_range_value + self.two_counter = 0 + super(MyRandom, self).__init__(*args, **kwargs) + + def randrange(self, start, stop=None, step=1, _int=int): + if start == 2: + answer = self.two_counter % 2 + self.two_counter += 1 + return answer + return self.large_range_value + + events = AdditiveEvents({1: 1, 2: 10 ** 1000}) + random_generator = MyRandom(1) + roller = Roller(events, random_generator) + alias_table = roller.alias_table + self.assertEqual(alias_table.height, 10 ** 1000 + 1) + + self.assertEqual(alias_table.get(0, 1), 1) + self.assertEqual(alias_table.get(1, 1), 2) + + expected = [1, 2, 1, 2, 1, 2, 1, 2, 1, 2] + self.assertEqual(expected, roller.roll_many(10)) + + # if you choose to mutate the random generator, I won't stop you. + roller.random_generator.large_range_value = 2 + self.assertEqual(alias_table.get(0, 2), 2) + self.assertEqual(alias_table.get(1, 2), 2) + expected = [2] * 10 + self.assertEqual(expected, roller.roll_many(10)) diff --git a/tests/tools/test_alias_table.py b/tests/tools/test_alias_table.py new file mode 100644 index 0000000..728b65f --- /dev/null +++ b/tests/tools/test_alias_table.py @@ -0,0 +1,97 @@ +import unittest + +from dicetables.tools.alias_table import AliasTable, Alias + + +class TestAliasTable(unittest.TestCase): + def test_alias(self): + primary = 1 + alternate = 2 + primary_height = 3 + alias = Alias(primary=primary, alternate=alternate, primary_height=primary_height) + self.assertEqual(alias.primary, primary) + self.assertEqual(alias.alternate, alternate) + self.assertEqual(alias.primary_height, primary_height) + + def assert_alias_table_has_expected_counts(self, alias_table, expected_counts): + actual_counts = dict.fromkeys(expected_counts.keys(), 0) + for length_val in range(alias_table.length): + for height_val in range(alias_table.height): + actual_counts[alias_table.get(length_val, height_val)] += 1 + + self.assertEqual(expected_counts, actual_counts) + + actual_primaries = {alias.primary for alias in alias_table.to_list()} + expected_primaries = set(expected_counts.keys()) + self.assertEqual(actual_primaries, expected_primaries) + + def test_alias_table(self): + input_dict = {1: 1, 3: 4, 5: 2} + expected_height = 1 + 4 + 2 + expected_length = 3 + + alias_table = AliasTable(input_dict) + + self.assertEqual(alias_table.height, expected_height) + self.assertEqual(alias_table.length, expected_length) + + expected_counts = {1: 3, 3: 12, 5: 6} + + self.assert_alias_table_has_expected_counts(alias_table, expected_counts) + + def test_even_alias_table_even_number(self): + input_dict = {1: 2, 2: 2, 3: 2, 4: 2} + expected_counts = {1: 8, 2: 8, 3: 8, 4: 8} + + alias_table = AliasTable(input_dict) + self.assertEqual(alias_table.length, 4) + self.assertEqual(alias_table.height, 8) + + self.assert_alias_table_has_expected_counts(alias_table, expected_counts) + + def test_even_alias_table_odd_number(self): + input_dict = {1: 2, 2: 2, 4: 2} + expected_counts = {1: 6, 2: 6, 4: 6} + + alias_table = AliasTable(input_dict) + self.assertEqual(alias_table.length, 3) + self.assertEqual(alias_table.height, 6) + self.assert_alias_table_has_expected_counts(alias_table, expected_counts) + + def test_alias_table_to_list(self): + input_dict = {1: 1, 2: 2, 3: 2, 4: 3} + alias_table = AliasTable(input_dict) + expected_list = [ + Alias(primary=1, alternate=4, primary_height=4), + Alias(primary=4, alternate=4, primary_height=8), + Alias(primary=3, alternate=3, primary_height=8), + Alias(primary=2, alternate=2, primary_height=8) + ] + self.assertEqual(alias_table.to_list(), expected_list) + + def test_alias_table_to_list_silly_test_so_that_i_can_visualize_Vose_algorithm_better(self): + input_dict = {1: 1, 2: 3, 3: 4, 4: 6, 5: 7} + alias_table = AliasTable(input_dict) + self.assertEqual(alias_table.height, 21) + self.assert_alias_table_has_expected_counts(alias_table, {1: 5, 2: 15, 3: 20, 4: 30, 5: 35}) + # small = [(1, 5), (2, 15), (3, 20)] + # big = [(4, 30), (5, 35)] + expected_list = [ + Alias(primary=3, alternate=5, primary_height=20), # small[(1, 5), (2, 15)], big[(4, 30), (5, 35-1)] + Alias(primary=2, alternate=5, primary_height=15), # small[(1, 5)], big[(4, 30), (5, 34-6)] + Alias(primary=1, alternate=5, primary_height=5), # small[(5, 28-16)], big[(4, 30)] + Alias(primary=5, alternate=4, primary_height=12), # small[], big[(4, 30-9)] + Alias(primary=4, alternate=4, primary_height=21) + ] + self.assertEqual(alias_table.to_list(), expected_list) + + def test_alias_table_to_list_does_not_mutate_alias_table(self): + input_dict = {1: 1} + alias_table = AliasTable(input_dict) + + alias_list = alias_table.to_list() + alias_list[0] = Alias(2, 2, 2) + self.assertNotEqual(alias_table.to_list(), alias_list) + + expected = [Alias(1, 1, 1)] + self.assertEqual(alias_table.to_list(), expected) diff --git a/tests/tools/test_dictcombiner.py b/tests/tools/test_dictcombiner.py index 0daf72f..3824b9d 100644 --- a/tests/tools/test_dictcombiner.py +++ b/tests/tools/test_dictcombiner.py @@ -42,6 +42,7 @@ def tearDown(self): } {'first method': {size of input dict: {combine times: size of Dictcombiner.get_dict(), ...}, ...}, ... } """ + def test_DictCombiner_get_fastest_method_DictCombiner_sized_one_and_one_times_never_picks_indexed_values(self): accepted_choices = ('dictionary', 'flattened_list') for power_of_two in range(10): @@ -226,7 +227,7 @@ def test_DictCombiner_combine_by_flattened_list_identity(self): to_combine = {1: 1, 2: 2} new_combiner = DictCombiner({0: 1}).combine_by_flattened_list(to_combine, 1) self.assertEqual(new_combiner.get_dict(), to_combine) - + def test_DictCombiner_combine_by_flattened_list__many_combines(self): to_combine = {1: 1, 2: 2} new_combiner = DictCombiner({0: 1}).combine_by_flattened_list(to_combine, 3) @@ -261,12 +262,12 @@ def test_DictCombiner_combine_by_flattened_list_complex_DictCombiner(self): {3: 1, 4: 4, 5: 4} + {4: 2, 5: 8, 6:8} = {3:1, 4: 6, 5: 12, 6: 8} """ self.assertEqual(new_combiner.get_dict(), {3: 1, 4: 6, 5: 12, 6: 8}) - + def test_DictCombiner_combine_by_indexed_values_identity(self): to_combine = {1: 1, 2: 2} new_combiner = DictCombiner({0: 1}).combine_by_indexed_values(to_combine, 1) self.assertEqual(new_combiner.get_dict(), to_combine) - + def test_DictCombiner_combine_by_indexed_values_many_combines(self): to_combine = {1: 1, 2: 2} new_combiner = DictCombiner({0: 1}).combine_by_indexed_values(to_combine, 3) diff --git a/tests/tools/test_listtostring.py b/tests/tools/test_listtostring.py index 302f007..4c38003 100644 --- a/tests/tools/test_listtostring.py +++ b/tests/tools/test_listtostring.py @@ -72,5 +72,6 @@ def test_get_string_from_list_returns_number_out_of_order(self): the_list = [5, 3, 7, 1, 3, 6, 8, 9, 10, 2] self.assertEqual(l2s.get_string_from_list_of_ints(the_list), '1-3, 5-10') + if __name__ == '__main__': unittest.main() diff --git a/tests/tools/test_numberformatter.py b/tests/tools/test_numberformatter.py index 0e5e0eb..31e9b2e 100644 --- a/tests/tools/test_numberformatter.py +++ b/tests/tools/test_numberformatter.py @@ -72,8 +72,8 @@ def test_NumberFormatter_get_exponent_int__big_small_pos_neg(self): formatter = NumberFormatter() self.assertEqual(formatter.get_exponent(50), 1) self.assertEqual(formatter.get_exponent(-50), 1) - self.assertEqual(formatter.get_exponent(5*10**1000), 1000) - self.assertEqual(formatter.get_exponent(-5*10**1000), 1000) + self.assertEqual(formatter.get_exponent(5 * 10 ** 1000), 1000) + self.assertEqual(formatter.get_exponent(-5 * 10 ** 1000), 1000) def test_NumberFormatter_get_exponent__rounding(self): four_digits = NumberFormatter() @@ -167,13 +167,13 @@ def test_NumberFormatter_format_as_exponent_not_remove_extra_zero(self): def test_NumberFormatter_format_as_exponent_remove_extra_zero(self): formatter = NumberFormatter() - test = 1.23456789*10**5 + test = 1.23456789 * 10 ** 5 self.assertEqual('{:.3e}'.format(test), '1.235e+05') self.assertEqual(formatter.format_exponent(test), '1.235e+5') def test_NumberFormatter_format_as_exponent_negative_number(self): formatter = NumberFormatter() - test = -1.23456789*10**5 + test = -1.23456789 * 10 ** 5 self.assertEqual(formatter.format_exponent(test), '-1.235e+5') def test_NumberFormatter_format_as_exponent_lt_one(self): @@ -189,29 +189,29 @@ def test_NumberFormatter_white_box_test_OverflowError_calls_new_func_does_not_ra def test_NumberFormatter_format_huge_int_pos_rounds(self): formatter = NumberFormatter() - test_1 = 123449*10**500 - test_2 = 123451*10**500 + test_1 = 123449 * 10 ** 500 + test_2 = 123451 * 10 ** 500 self.assertEqual(formatter.format_exponent(test_1), '1.234e+505') self.assertEqual(formatter.format_exponent(test_2), '1.235e+505') def test_NumberFormatter_format_huge_int_neg_rounds(self): formatter = NumberFormatter() - test_1 = -123449*10**500 - test_2 = -123451*10**500 + test_1 = -123449 * 10 ** 500 + test_2 = -123451 * 10 ** 500 self.assertEqual(formatter.format_exponent(test_1), '-1.234e+505') self.assertEqual(formatter.format_exponent(test_2), '-1.235e+505') def test_NumberFormatter_format_huge_int_round_to_next_power(self): four_digits = NumberFormatter() eight_digits = NumberFormatter(shown_digits=8) - test = 999951*10**500 + test = 999951 * 10 ** 500 self.assertEqual(four_digits.format_exponent(test), '1.000e+506') self.assertEqual(eight_digits.format(test), '9.9995100e+505') def test_NumberFormatter_format_huge_int_round_to_next_power_negative_number(self): four_digits = NumberFormatter() eight_digits = NumberFormatter(shown_digits=8) - test = -999951*10**500 + test = -999951 * 10 ** 500 self.assertEqual(four_digits.format_exponent(test), '-1.000e+506') self.assertEqual(eight_digits.format(test), '-9.9995100e+505') @@ -239,8 +239,8 @@ def test_NumberFormatter_format__min_fixed_pt_exp_at_zero_max_comma_exp_at_neg_o def test_NumberFormatter_max_comma_exp_ridiculous_case(self): test = NumberFormatter(max_comma_exp=1000) - num = int('9'*999) - num_string = '999,'*333 + num = int('9' * 999) + num_string = '999,' * 333 num_string = num_string.rstrip(',') self.assertEqual(test.format(num), num_string) @@ -282,7 +282,7 @@ def test_NumberFormatter_format_ints(self): self.assert_format_number(-99999999, '-1.000e+8') self.assert_format_number(999999999, '1.000e+9') self.assert_format_number(9999999999, '1.000e+10') - self.assert_format_number(123451*10**1000, '1.235e+1005') + self.assert_format_number(123451 * 10 ** 1000, '1.235e+1005') def test_NumberFormatter_is_special_case_zero(self): self.assertTrue(NumberFormatter().is_special_case(0)) @@ -298,7 +298,7 @@ def test_NumberFormatter_is_special_case_neg_infinity(self): def test_NumberFormatter_is_special_case_false(self): self.assertFalse(NumberFormatter().is_special_case(1.23)) - self.assertFalse(NumberFormatter().is_special_case(-10*1111)) + self.assertFalse(NumberFormatter().is_special_case(-10 * 1111)) self.assertFalse(NumberFormatter().is_special_case(Decimal('1e-1000'))) self.assertFalse(NumberFormatter().is_special_case(Decimal('1e+1000'))) self.assertFalse(NumberFormatter().is_special_case(Decimal('-1e+1000'))) diff --git a/tests/tools/test_orderedcombinations.py b/tests/tools/test_orderedcombinations.py index 8247d65..6f68f2c 100644 --- a/tests/tools/test_orderedcombinations.py +++ b/tests/tools/test_orderedcombinations.py @@ -62,9 +62,9 @@ def test_count_number_of_combinations_all_values_one_different_groupings(self): def test_count_number_of_combination_different_values(self): to_use = {key: key for key in range(1, 11)} self.assertEqual(get_combination_occurrences((1, 2), to_use), 4) - self.assertEqual(get_combination_occurrences((1, 2, 2, 3), to_use), 12*4*3) - self.assertEqual(get_combination_occurrences((2, 2, 3, 3), to_use), 6*4*9) - self.assertEqual(get_combination_occurrences((5, 5, 10, 10), to_use), 6*25*100) + self.assertEqual(get_combination_occurrences((1, 2, 2, 3), to_use), 12 * 4 * 3) + self.assertEqual(get_combination_occurrences((2, 2, 3, 3), to_use), 6 * 4 * 9) + self.assertEqual(get_combination_occurrences((5, 5, 10, 10), to_use), 6 * 25 * 100) def test_ordered_combinations_of_events_all_single_weight_simple_case(self): answer = ordered_combinations_of_events(AdditiveEvents({1: 1, 2: 1}), 3) diff --git a/tests/tools/test_pyXfuncs.py b/tests/tools/test_pyXfuncs.py index 9ec7cb5..b2b0e1e 100644 --- a/tests/tools/test_pyXfuncs.py +++ b/tests/tools/test_pyXfuncs.py @@ -4,6 +4,7 @@ import unittest from sys import version_info + if version_info[0] < 3: import dicetables.tools.py2funcs as pf else: @@ -16,13 +17,13 @@ def test_is_int_true_small(self): self.assertTrue(pf.is_int(10)) def test_is_int_true_big(self): - self.assertTrue(pf.is_int(10**1000)) + self.assertTrue(pf.is_int(10 ** 1000)) def test_is_int_true_small_neg(self): self.assertTrue(pf.is_int(-10)) def test_is_int_true_big_neg(self): - self.assertTrue(pf.is_int(-1*10**1000)) + self.assertTrue(pf.is_int(-1 * 10 ** 1000)) def test_is_int_true_zero(self): self.assertTrue(pf.is_int(0)) @@ -34,4 +35,3 @@ def test_is_int_false_other(self): self.assertFalse(pf.is_int('a')) self.assertFalse(pf.is_int({})) self.assertFalse(pf.is_int([])) - diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..42298b0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = py27,py37,py36 + +[testenv] +deps = + coverage + +commands = + coverage run -a -m unittest discover + + python doctests.py + + coverage report -m + + +