diff --git a/plugins/gaming.py b/plugins/gaming.py index 0a6d1e699..5cdf77636 100644 --- a/plugins/gaming.py +++ b/plugins/gaming.py @@ -5,6 +5,7 @@ Modified By: - Luke Rogers + - leonthemisfit License: GPL v3 @@ -15,41 +16,127 @@ from cloudbot import hook +# String constants for the coin flip function +INVALID_NUMBER = "Invalid input {!r}: not a number" +NO_COIN = "makes a coin flipping motion" +SINGLE_COIN = "flips a coin and gets {}." +MANY_COINS = "flips {} coins and gets {} heads and {} tails." + +# Pregenerated mean and variance for fudge dice +FUDGE_MEAN = 0 +FUDGE_VAR = 0.6667 + +INVALID_ROLL = "Invalid dice roll {!r}" + +ROLL_LIMIT = 100 # The maximum number of times to roll or flip before approximating results + whitespace_re = re.compile(r'\s+') valid_diceroll = re.compile(r'^([+-]?(?:\d+|\d*d(?:\d+|F))(?:[+-](?:\d+|\d*d(?:\d+|F)))*)( .+)?$', re.I) sign_re = re.compile(r'[+-]?(?:\d*d)?(?:\d+|F)', re.I) split_re = re.compile(r'([\d+-]*)d?(F|\d*)', re.I) -def clamp(n, min_value, max_value): - """Restricts a number to a certain range of values, - returning the min or max value if the value is too small or large, respectively - :param n: The value to clamp - :param min_value: The minimum possible value - :param max_value: The maximum possible value - :return: The clamped value +def find_midpoint(sides, roll_cnt): + """find the midpoint for a die with n rolls + + :type sides: int + :type roll_cnt: int + :rtype: float + + >>> [find_midpoint(s, r) for s, r in [(6, 1), (6, 2), (12, 1), (12, 2)]] + [3.5, 7.0, 6.5, 13.0] """ - return min(max(n, min_value), max_value) + return 0.5 * (sides + 1) * roll_cnt -def n_rolls(count, n): - """roll an n-sided die count times - :type count: int - :type n: int | str +def find_variance(sides): + """find the variance for a die with n sides + + :type sides: int + :rtype: float + + >>> [find_variance(s) for s in [3, 4, 5, 6, 20, 50]] + [0.6667, 1.25, 2.0, 2.9167, 33.25, 208.25] + """ + return round((sides ** 2 - 1) / 12, 4) + + +def find_mid_var(sides, roll_cnt): + """find the midpoint and variance for a die with x sides and y rolls + + :type sides: int + :type roll_cnt: int + :rtype: (float, float) + + >>> [find_mid_var(s, r) for s, r in [(6, 1), (6, 2), (12, 1), (12, 2)]] + [(3.5, 2.9167), (7.0, 2.9167), (6.5, 11.9167), (13.0, 11.9167)] + """ + return find_midpoint(sides, roll_cnt), find_variance(sides) + + +def find_adjusted_variance(variance, roll_cnt): + """find the variance adjusted for the number of rolls + + :type variance: float + :type roll_cnt: int + :rtype: float + + >>> [find_adjusted_variance(find_variance(s), r) for s, r in [(6, 1), (6, 2), (12, 1), (12, 2)]] + [1.7078, 2.4152, 3.4521, 4.8819] + """ + return round((variance * roll_cnt) ** 0.5, 4) + + +def approximate_rolls(roll_cnt, sides, fudge): + """approximate a sum based on a random normal variate using the midpoint as the mu and variance as the sigma + + :type roll_cnt: int + :type sides: int | str + :type fudge: bool + :rtype: list(int)""" + if fudge: + mid = FUDGE_MEAN + var = FUDGE_VAR + else: + mid, var = find_mid_var(sides, roll_cnt) + + adj_var = find_adjusted_variance(var, roll_cnt) + + return [round(random.normalvariate(mid, adj_var))] + + +def simulate_rolls(roll_cnt, sides, fudge): + """simulate rolling a dice + + :type roll_cnt: int + :type sides: int | str + :type fudge: bool + :rtype: list(int) """ - if n in ('f', 'F'): - return [random.randint(-1, 1) for _ in range(min(count, 100))] + if fudge: + lower = -1 + upper = 1 + else: + lower, upper = sorted((sides, 1)) - if count < 100: - return [random.randint(1, n) for _ in range(count)] + return [random.randint(lower, upper) for _ in range(roll_cnt)] - # Calculate a random sum approximated using a randomized normal variate with the midpoint used as the mu - # and an approximated standard deviation based on variance as the sigma - mid = .5 * (n + 1) * count - var = (n ** 2 - 1) / 12 - adj_var = (var * count) ** 0.5 - return [int(random.normalvariate(mid, adj_var))] +def n_rolls(roll_cnt, sides): + """roll an n-sided die count times + + :type roll_cnt: int + :type sides: int | str + :rtype: list[int] + """ + fudge = sides in ('f', 'F') + + if roll_cnt < ROLL_LIMIT: + values = simulate_rolls(roll_cnt, sides, fudge) + else: + values = approximate_rolls(roll_cnt, sides, fudge) + + return values @hook.command("roll", "dice") @@ -58,7 +145,6 @@ def dice(text, notice): :type text: str """ - if hasattr(text, "groups"): text, desc = text.groups() else: # type(text) == str @@ -66,7 +152,7 @@ def dice(text, notice): if match: text, desc = match.groups() else: - notice("Invalid dice roll '{}'".format(text)) + notice(INVALID_ROLL.format(text)) return if "d" not in text: @@ -74,7 +160,7 @@ def dice(text, notice): spec = whitespace_re.sub('', text) if not valid_diceroll.match(spec): - notice("Invalid dice roll '{}'".format(text)) + notice(INVALID_ROLL.format(text)) return groups = sign_re.findall(spec) @@ -138,24 +224,23 @@ def coin(text, notice, action): :type text: str """ - + amount = 1 if text: try: amount = int(text) except (ValueError, TypeError): - notice("Invalid input '{}': not a number".format(text)) + notice(INVALID_NUMBER.format(text)) return - else: - amount = 1 - if amount == 1: - action("flips a coin and gets {}.".format(random.choice(["heads", "tails"]))) - elif amount == 0: - action("makes a coin flipping motion") + if amount == 0: + action(NO_COIN) + elif amount == 1: + side = random.choice(['heads', 'tails']) + action(SINGLE_COIN.format(side)) else: - mu = .5 * amount - sigma = (.75 * amount) ** .5 - n = random.normalvariate(mu, sigma) - heads = clamp(int(round(n)), 0, amount) + if amount < ROLL_LIMIT: + heads = sum(random.randint(0, 1) for _ in range(amount)) + else: + heads = round(amount * random.uniform(0.45, 0.55)) tails = amount - heads - action("flips {} coins and gets {} heads and {} tails.".format(amount, heads, tails)) + action(MANY_COINS.format(amount, heads, tails))