Skip to content
This repository was archived by the owner on Oct 23, 2019. It is now read-only.
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d62011f
Updated gaming plugin docstrings
leonthemisfit Feb 5, 2019
716ac87
Refactored coin function to use string constants
leonthemisfit Feb 5, 2019
b03af56
Rewrote coin flip to use a cleaner method
leonthemisfit Feb 5, 2019
81a0d70
Removed clamp function from gaming plugin
leonthemisfit Feb 5, 2019
f94fed1
Added leonthemisfit to modified by
leonthemisfit Feb 5, 2019
8dd899f
Fixed comparison typo and added roll limit const to gaming plugin
leonthemisfit Feb 5, 2019
87c029f
Updated format strings to remove ' literals and added invalid roll const
leonthemisfit Feb 5, 2019
5306151
Fixed incorrect roll count comparison
leonthemisfit Feb 6, 2019
46702b9
Added fudge approximation and refactored to match change
leonthemisfit Feb 6, 2019
7d40650
Patches bug with bounds checking in .dice command
leonthemisfit Feb 6, 2019
5051659
Fixed fudge dice issues with simulated rolls
leonthemisfit Mar 18, 2019
29fd437
Moved approximation explanation to a clearer location
leonthemisfit Mar 18, 2019
a6ed38c
Refactored n_rolls to have clearer param names
leonthemisfit Mar 18, 2019
fcb5ce2
Changed calls to int to calls to round
leonthemisfit Mar 20, 2019
c32a1c3
Added returned type to nrolls
leonthemisfit Mar 20, 2019
1c71176
Moved approximation formulas to separate functions
leonthemisfit Mar 20, 2019
7dc404e
Moved approximation to separate function
leonthemisfit Mar 20, 2019
1036375
Moved dice roll simulation to its own function
leonthemisfit Mar 20, 2019
a0c4984
Refactored n_rolls to follow SESE
leonthemisfit Mar 20, 2019
cc2dfa9
Rounded results of find_variance function
leonthemisfit Mar 20, 2019
685be6a
Rounded the result of find_adjusted_variance()
leonthemisfit Mar 20, 2019
f4834c0
Added tests to utility function docstrings
leonthemisfit Mar 20, 2019
46245e9
Updated tests so they correct pass in travis
leonthemisfit Mar 20, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 123 additions & 38 deletions plugins/gaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

Modified By:
- Luke Rogers <https://github.com/lukeroge>
- leonthemisfit <https://github.com/leonthemisfit>

License:
GPL v3
Expand All @@ -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")
Expand All @@ -58,23 +145,22 @@ def dice(text, notice):

:type text: str
"""

if hasattr(text, "groups"):
text, desc = text.groups()
else: # type(text) == str
match = valid_diceroll.match(whitespace_re.sub("", text))
if match:
text, desc = match.groups()
else:
notice("Invalid dice roll '{}'".format(text))
notice(INVALID_ROLL.format(text))
return

if "d" not in text:
return

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)

Expand Down Expand Up @@ -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))