Example #1
0
def test_pool():
    """
    Take a bucket of DRVs, and consider the results irrespective of order.
    """
    pool = pools.pool(d6, count=2)
    assert p(pool == pools.PlainResult(1, 1)) == Fraction(1, 36)
    assert p(pool == pools.PlainResult(1, 2)) == Fraction(2, 36)
def test_difficulty(diff):
    """
    Setting a difficulty means that some matching sets don't count.
    """
    pool = ore.matches(2, difficulty=diff)
    assert p(pool.apply(len) > 0) == Fraction(11 - diff, 100)
    # You wouldn't normally allow_highest and then require a matching set, but
    # since the probability is the same, we may as well check it.
    pool = ore.matches(2, difficulty=diff, allow_highest=True)

    def success(matches):
        return matches[0].width > 1

    assert p(pool.apply(success)) == Fraction(11 - diff, 100)

    # If you rolled at least one die over the difficulty, but still failed,
    # then the difficulty actually makes no difference to your distribution.
    def failure(matches):
        return not success(matches) and matches[0].height >= diff

    assert pool.given(failure).is_same(
        ore.matches(2, allow_highest=True).given(failure))

    # That leaves cases where all dice were less than the difficulty. The
    # probability and distribution of that is an easy one to check.
    def woeful(matches):
        return not success(matches) and matches[0].height < diff

    assert p(pool.apply(woeful)) == Fraction(diff - 1, 10)**2
    if diff > 1:
        assert pool.given(woeful).apply(
            lambda matches: matches[0].height).is_same(
                # The maximum of uniformly-distributed values below diff.
                Pool(d(diff - 1), count=2).apply(max))
def test_pool_examples():
    """
    Check the examples of using pool() do vaguely work.
    """

    # First example of penalty die
    def penalty(result):
        return PlainResult(*(tuple(result)[1:]))

    penalised = ore.pool(1).apply(penalty).apply(ore.Match.get_matches)
    assert penalised.is_same(DRV({(): 1}))
    penalised = ore.pool(2).apply(penalty).apply(
        ore.Match.get_matches_or_highest)
    assert penalised.apply(len).is_same(DRV({1: 1}))
    assert penalised.apply(lambda x: x[0]).is_same(
        keep_lowest(1, d10,
                    count=2).apply(lambda x: ore.Match(1, next(iter(x)))))

    # Second example of penalty die
    def penalty(result):
        matches = sorted(
            ore.Match.get_all_sets(result),
            key=lambda x: (x.width, x.height),
        )
        matches[0] = ore.Match(matches[0].width - 1, matches[0].height)
        return PlainResult(*(die for match in matches
                             for die in [match.height] * match.width))

    penalised = ore.pool(1).apply(penalty).apply(ore.Match.get_matches)
    assert penalised.is_same(DRV({(): 1}))
    penalised = ore.pool(3).apply(penalty).apply(ore.Match.get_matches)
    # Discarding from the narrowest match of 3 dice doesn't affect your chance
    # of success! Width 3 becomes width 2, and width 2 means there's an
    # unmatched third die to discard.
    assert p(penalised.apply(len) > 0) == p(ore.matches(3).apply(len) > 0)
Example #4
0
def test_pool_addition():
    """
    You can add a constant, DRV or pool to a pool, and the effect is of
    including one or more extra dice in the pool.
    """
    pool = pools.pool(d6)
    assert p(pool + 1 == pools.PlainResult(1, 1)) == Fraction(1, 6)
    assert p(pool + d6 == pools.PlainResult(1, 1)) == Fraction(1, 36)
    assert p(pool + pool == pools.PlainResult(1, 1)) == Fraction(1, 36)
Example #5
0
def test_mixed_pool():
    """
    Not all dice in pool need to be the same, and you can build up a pool one
    item at a time if you want to.
    """
    pool = pools.pool(d6, d8)
    assert p(pool == pools.PlainResult(1, 1)) == Fraction(1, 48)
    assert p(pool == pools.PlainResult(1, 2)) == Fraction(2, 48)
    assert p(pool == pools.PlainResult(6, 7)) == Fraction(1, 48)
    assert pool.is_same(pools.pool(d6) + d8)
Example #6
0
def test_keep_lowest():
    """
    Roll N, keep the worst K of some DRV.
    """
    pool = pools.keep_lowest(2, d6, count=3)
    assert p(pool == pools.PlainResult(6, 6)) == Fraction(1, 216)
    # There are three ways each to get 1, 1, x for x = 2..6, plus 1, 1, 1.
    assert p(pool == pools.PlainResult(1, 1)) == Fraction(16, 216)
    pool0 = pools.keep_lowest(0, d6, count=10)
    assert pool0.is_same(DRV({pools.PlainResult(): 1}))
Example #7
0
def test_given():
    """
    Conditional probability distribution, given that some predicate is true.
    """
    var = DRV({x: 0.125 for x in range(8)})
    var_odd = var.given(lambda x: x % 2 != 0)
    var_even = var.given(lambda x: x % 2 == 0)
    assert p(var_odd == 2) == 0
    assert p(var_odd == 1) == 0.25
    assert p(var_even == 2) == 0.25
    assert p(var_even == 1) == 0
    var_square = var.given(lambda x: int(sqrt(x))**2 == x)
    assert p(var_square == 0) == pytest.approx(1 / 3)
    with pytest.raises(ZeroDivisionError):
        var.given(lambda x: x == 8)
def test_belle_curve():
    """
    There's a helpful table in the Godlike rulebook giving the probability of
    at least one match. This is based on the number of possible permutations of
    unmatched numbers that can show up. There's only so many ways that N values
    in the range 1-10 can all be different from each other.
    """
    assert p(ore.matches(1).apply(len) > 0) == 0
    assert p(ore.matches(2).apply(len) > 0) == Fraction(1, 10)
    assert p(ore.matches(3).apply(len) > 0) == Fraction(28, 100)

    # The table is approximate after this
    def combinations(d: int) -> int:
        # This is the combinatorics bit.
        # 10 choose d for the different sets of values, times d! for the orders
        # you could roll them in.
        return math.factorial(10) // math.factorial(10 - d)

    result = p(ore.matches(4).apply(len) > 0)
    assert 0.495 <= result < 0.505
    assert 1 - result == Fraction(combinations(4), 10**4)
    if SIZE_LIMIT >= 5:
        result = p(ore.matches(5).apply(len) > 0)
        assert 0.695 <= result < 0.705
        assert 1 - result == Fraction(combinations(5), 10**5)
    if SIZE_LIMIT >= 6:
        result = p(ore.matches(6).apply(len) > 0)
        assert 0.845 <= result < 0.855
        assert 1 - result == Fraction(combinations(6), 10**6)
    if SIZE_LIMIT >= 7:
        # Book got this one wrong: it says 93% but it's 93.952%
        result = p(ore.matches(7).apply(len) > 0)
        assert 0.93 <= result < 0.94
        assert 1 - result == Fraction(combinations(7), 10**7)
    if SIZE_LIMIT >= 8:
        result = p(ore.matches(8).apply(len) > 0)
        assert 0.975 <= result < 0.985
        assert 1 - result == Fraction(combinations(8), 10**8)
    if SIZE_LIMIT >= 9:
        result = p(ore.matches(9).apply(len) > 0)
        assert 0.9955 <= result < 0.9965
        assert 1 - result == Fraction(combinations(9), 10**9)
    if SIZE_LIMIT >= 10:
        # Book rounded this one wrong too, it's actually 99.963712%
        result = p(ore.matches(10).apply(len) > 0)
        assert 0.9985 <= result
        assert 1 - result == Fraction(combinations(10), 10**10)
Example #9
0
def test_unstoppable_explosion():
    """
    An exploded 6 counts as unstoppable.
    """
    assert p(
        ote.pool(1, explode=True,
                 unstoppable=True).apply(lambda x: x.unstoppable)) == Fraction(
                     1, 6)
Example #10
0
def test_wiggle():
    """
    Wiggle dice have the potential to be difficult to implement. The
    documentation lays out what's available to handle them.
    """
    # First example of 'wd' param.
    drv = ore.matches(3, wd=lambda x: PlainResult(max(x), max(x)))
    assert drv.is_same(
        Pool(d10,
             count=3).apply(lambda x: x + PlainResult(max(x), max(x))).apply(
                 ore.Match.get_matches))
    assert p(drv.apply(lambda x: ore.Match(5, 10) in x)) == Fraction(1, 1000)
    # Second example of 'wd' param
    drv = ore.matches(3, wd=lambda x, y=PlainResult(10, 9): y)
    assert drv.is_same(
        Pool(d10, count=3).apply(lambda x: x + PlainResult(10, 9)).apply(
            ore.Match.get_matches))
    assert p(drv.apply(lambda x: ore.Match(4, 10) in x)) == Fraction(1, 1000)
Example #11
0
def test_p():
    """
    The p function returns the probability that a boolean DRV is True.
    """
    coins = (10 @ DRV({0: 0.5, 1: 0.5}))
    assert drv.p(coins <= 0) == 0.5**10
    assert drv.p(coins >= 10) == 0.5**10
    assert drv.p(coins >= 5) > 0.5
    assert drv.p(coins >= 5) + drv.p(coins < 5) == 1
    # Non-boolean input is rejected, even though 0 == False and 1 == True
    with pytest.raises(TypeError):
        drv.p(coins)
    # It still works when True (or False) is missing.
    assert drv.p(DRV({False: 1})) == 0
    assert drv.p(DRV({True: 1})) == 1
Example #12
0
def test_keep_highest():
    """
    Roll N, keep the best K of some DRV.
    """
    pool = pools.keep_highest(2, d6, count=3)
    # There are three ways each to get 6, 6, x for x = 1..5, plus 6, 6, 6.
    assert p(pool == pools.PlainResult(6, 6)) == Fraction(16, 216)
    assert p(pool == pools.PlainResult(1, 1)) == Fraction(1, 216)
    # count=1000 acts as a performance test: if the implementation tries to
    # compute all possibilities and then restrict to 0 dice, it will fail.
    pool0 = pools.keep_highest(0, d6, count=1000)
    assert pool0.is_same(DRV({pools.PlainResult(): 1}))
    # Examples from docs
    poolA = pools.keep_highest(2, d6) + d6 + d6
    poolB = pools.pool(d6, count=3)
    poolC = pools.keep_highest(2, d6, count=3)
    poolD = pools.pool(d6, result_type=pools.KeepHighest(2)) + d6 + d6
    assert poolA.is_same(poolB)
    assert not poolA.is_same(poolC)
    assert poolD.is_same(poolC)
Example #13
0
def test_revised_botch():
    """
    The botch rule is different in Revised. Now if you roll at least one
    success, you cannot botch, even if it's cancelled out by 1s.
    """
    # With one or two dice, Revised makes no difference from 1st/2nd.
    die = storyteller.revised_standard(6).apply(storyteller.total)
    probs = {
        -1: 0.1,
        0: 0.4,
        1: 0.5,
    }
    assert die.is_close(DRV(probs))
    roll2 = (2 @ storyteller.revised_standard(6)).apply(storyteller.total)
    # With 2 dice, the only rolls that botch are 1+1 .. 1+5 and 2+1 .. 5+1
    assert p(roll2 < 0) == pytest.approx(0.09)
    # It's three dice where the difference kicks in: the case of 2 botches and
    # one success has probability 0.1 * 0.1 * 0.5 * 3 = 0.015. In Revised
    # that's not a botch.
    old_roll = 3 @ storyteller.standard(6)
    new_roll = (3 @ storyteller.revised_standard(6)).apply(storyteller.total)
    assert p(old_roll == -1) == pytest.approx(p(new_roll == -1) + 0.015)
Example #14
0
def test_unstoppable(dice):
    """
    unstoppable() can be applied to a pool to tell you both the total and the
    existence (or not) of 6s.
    """
    drv = ote.pool(dice).apply(ote.Unstoppable)
    drv2 = ote.pool(dice, unstoppable=True)
    drv3 = drv2.apply(ote.Unstoppable)
    assert drv.is_same(drv2)
    assert drv.is_same(drv3)
    assert drv.apply(lambda x: x.total).is_same(ote.total(dice))
    # Check that you can apply(sum) regardless of options.
    assert drv.apply(lambda x: x.total).is_same(drv.apply(sum))
    assert p(drv.apply(lambda x: not x.unstoppable)) == Fraction(5, 6)**dice
Example #15
0
def _check(
        result,
        values,
        botch=Fraction(1, 6),
        single=True,
        totals=None,
        highest=None,
):
    if totals is None:
        totals = values
    if highest is None:
        highest = totals
    assert result.apply(type).is_same(DRV({opend6.Result: 1}))
    assert result.apply(int).is_same(values)
    # Test the fields of Result
    assert result.apply(lambda x: x.total).is_same(totals)
    assert result.apply(lambda x: x.highest).is_same(highest)
    assert p(result.apply(lambda x: x.botch)) == botch
    assert result.apply(lambda x: x.singleton).is_same(DRV({single: 1}))
Example #16
0
def test_botch_cancels():
    """Optionally you can switch off the botch-cancels rule."""
    result = opend6.total(2, botch_cancels=False)

    def botch(x):
        return x.botch

    def nobotch(x):
        return not x.botch

    assert result.apply(lambda x: x.total).is_same(d6 + d6.explode())
    assert p(result.apply(botch)) == Fraction(1, 6)
    assert result.given(botch).apply(lambda x: x.total).is_same(d6 + 1)
    assert result.given(nobotch).apply(lambda x: x.total).is_same(
        d6 + d6.explode().given(lambda x: x != 1))
    # We're happy with the result of total(), so we can use it to check a few
    # results of test()
    for target in range(0, 20, 5):
        success = opend6.test(2, target, botch_cancels=False)
        assert success.apply(lambda x: x.success).is_same(
            result.apply(lambda x: x.total >= target))
Example #17
0
def test_hard():
    """
    Hard dice can be added to the pool.
    """
    # With d+hd, there's only one possible way to get a match
    assert ore.matches(d=1, hd=1).is_close(
        DRV({
            (ore.Match(2, 10), ): 1 / 10,
            (): 9 / 10,
        }))
    # 2d + hd is still small enough to figure out easily...
    assert ore.matches(d=2, hd=1).is_close(
        DRV({
            (ore.Match(3, 10), ): 1 / 100,
            (ore.Match(2, 10), ): 18 / 100,
            **{(ore.Match(2, n), ): 1 / 100
               for n in range(1, 10)},
            (): 72 / 100,
        }))
    # 3d + hd: now we can get two pairs of 10 and something else.
    result = p(ore.matches(d=3, hd=1).apply(len) == 2)
    assert result == pytest.approx(9 * 3 / 1000)
Example #18
0
def test_explode():
    """
    The "Blowing the top" rule doesn't require any special result values, it
    just adds an exploding d6 sometimes.
    """
    # Probabilities for explosion on 2 dice, with bonus/penalty
    normal_prob = Fraction(1, 36)
    bonus_prob = Fraction(1 + 3 * 5, 216)
    pen_prob = Fraction(1, 216)

    def low(result):
        return result < 12

    def high(result):
        return result >= 12

    assert ote.total(2, explode=True).given(low).is_same((2 @ d6).given(low))
    assert ote.total(2, explode=True).given(high).is_same(d6.explode() + 12, )
    assert ote.total(2, explode=5).given(low).is_same((2 @ d6).given(low))
    assert ote.total(2, explode=5).given(high).is_same(
        d6.explode(rerolls=5) + 12, )
    assert p(ote.total(2, bonus=0, explode=True) > 12) == normal_prob
    assert p(ote.total(2, bonus=1, explode=True) > 12) == bonus_prob
    assert p(ote.total(2, bonus=-1, explode=True) > 12) == pen_prob

    def short(result):
        return len(result.values) == 2

    def long(result):
        return len(result.values) == 3

    assert ote.pool(2, explode=True).given(short).is_same(
        Pool(d6, d6).given(lambda x: x != PlainResult(6, 6)))
    assert ote.pool(2, explode=True).given(long).is_same(
        (d6.explode().apply(lambda x: PlainResult(6, 6, x))), )
    assert ote.pool(2, explode=5).given(short).is_same(
        Pool(d6, d6).given(lambda x: x != PlainResult(6, 6)))
    assert ote.pool(2, explode=5).given(long).is_same(
        (d6.explode(rerolls=5).apply(lambda x: PlainResult(6, 6, x))), )
    assert p(ote.pool(2, bonus=0, explode=True).apply(sum) > 12) == normal_prob
    assert p(ote.pool(2, bonus=1, explode=True).apply(sum) > 12) == bonus_prob
    assert p(ote.pool(2, bonus=-1, explode=True).apply(sum) > 12) == pen_prob
Example #19
0
def test_botch():
    """
    total() reports a botch as -1. pool() reports a single -1.
    """
    # Probabilities for botch on 2 dice, with bonus/penalty
    normal_prob = Fraction(1, 36)
    bonus_prob = Fraction(1, 216)
    pen_prob = Fraction(1 + 3 * 5, 216)
    assert ote.total(2, botch=True).is_same(
        (2 @ d6).apply(lambda x: -1 if x == 2 else x))
    assert p(ote.total(2, bonus=0, botch=True) == -1) == normal_prob
    assert p(ote.total(2, bonus=1, botch=True) == -1) == bonus_prob
    assert p(ote.total(2, bonus=-1, botch=True) == -1) == pen_prob

    botch = PlainResult(-1)
    assert ote.pool(2, botch=True).is_same(
        Pool(d6, d6).apply(lambda x: botch if x == PlainResult(1, 1) else x))
    assert p(ote.pool(2, bonus=0, botch=True) == botch) == normal_prob
    assert p(ote.pool(2, bonus=1, botch=True) == botch) == bonus_prob
    assert p(ote.pool(2, bonus=-1, botch=True) == botch) == pen_prob