rollcake bot
This commit is contained in:
parent
02586f1d34
commit
86e4c583b4
170
zulip_bots/zulip_bots/bots/rollcake/rollcake.py
Normal file
170
zulip_bots/zulip_bots/bots/rollcake/rollcake.py
Normal file
|
@ -0,0 +1,170 @@
|
|||
import random
|
||||
import re
|
||||
|
||||
USAGE = '''@ me to roll RPG dice. Examples:
|
||||
|
||||
@**Rollcake** 2d6+2
|
||||
|
||||
@**Rollcake** pbta -1 adv
|
||||
*(Powered by the Apocalypse, -1 penalty, roll with advantage: 3d6, add highest 2, subtract 1)*
|
||||
|
||||
@**Rollcake** fitd 2
|
||||
*(Forged in the Dark, action rating 2: 2d6, take highest)*
|
||||
|
||||
@**Rollcake** sbr 3 danger
|
||||
*(Sparked by the Resistance, dice pool 3, dangerous: 3d10, remove highest 2, take highest remaining)*
|
||||
|
||||
@**Rollcake** void 3
|
||||
*(Voidheart Symphony city roll, stress gauge 3: 2d6, check if either/both are above 3)*
|
||||
'''
|
||||
|
||||
PBTA_RE = re.compile(r'(pbta|apoc(alypse)?|void(heart)? castle)\s+(?P<mod>[-+]?[0-9])(\s+(?P<adv>adv|dis))?')
|
||||
FITD_RE = re.compile(r'(fitd|forged)\s+(?P<rating>[0-9])\b')
|
||||
SBR_RE = re.compile(r'(sbr|res(istance)?)\s+(?P<pool>[0-9])(\s+(?P<risk>risk|dang))?')
|
||||
VOID_RE = re.compile(r'(void(heart)?( city)?)\s+(?P<stress>[0-6])(\s+(?P<adv>adv|dis))?')
|
||||
DICE_RE = re.compile(r'\b(?P<count>[0-9]*)d(?P<sides>[0-9]+)\s*(?P<mod>[-+][0-9]+)?')
|
||||
|
||||
def roll(count, sides):
|
||||
return [random.randint(1, sides) for i in range(count)]
|
||||
|
||||
def handle_roll(content):
|
||||
content = content.lower()
|
||||
|
||||
dice = DICE_RE.search(content)
|
||||
if dice:
|
||||
count = int(dice.group('count') or '1')
|
||||
sides = int(dice.group('sides'))
|
||||
mod = int(dice.group('mod') or '0')
|
||||
results = roll(count, sides)
|
||||
total = sum(results) + mod
|
||||
return 'Total: **{}**\n(max {}, min {})\nRolls: {}'.format(
|
||||
total,
|
||||
max(results),
|
||||
min(results),
|
||||
', '.join(str(n) for n in results),
|
||||
)
|
||||
|
||||
pbta = PBTA_RE.search(content)
|
||||
if pbta:
|
||||
mod = int(pbta.group('mod') or '0')
|
||||
adv = pbta.group('adv')
|
||||
if adv:
|
||||
results = roll(3, 6)
|
||||
idx = results.index(min(results) if adv == 'adv' else max(results))
|
||||
else:
|
||||
results = roll(2, 6)
|
||||
idx = -1
|
||||
total = sum(n for (i, n) in enumerate(results) if i != idx) + mod
|
||||
if total >= 10:
|
||||
outcome = 'Strong hit'
|
||||
elif total >= 7:
|
||||
outcome = 'Weak hit'
|
||||
else:
|
||||
outcome = 'Miss'
|
||||
return '**{}**\n**{}**\nRolls: {}'.format(
|
||||
outcome,
|
||||
total,
|
||||
', '.join(
|
||||
('~~{}~~' if i == idx else '{}').format(n)
|
||||
for (i, n) in enumerate(results)
|
||||
)
|
||||
)
|
||||
|
||||
void = VOID_RE.search(content)
|
||||
if void:
|
||||
stress = int(void.group('stress'))
|
||||
adv = void.group('adv')
|
||||
if adv:
|
||||
results = roll(3, 6)
|
||||
idx = results.index(min(results) if adv == 'adv' else max(results))
|
||||
else:
|
||||
results = roll(2, 6)
|
||||
idx = -1
|
||||
count = sum(1 for (i, n) in enumerate(results) if i != idx and n > stress)
|
||||
return '**{}**\nRolls: {}'.format(
|
||||
['Miss', 'Weak hit', 'Strong hit'][count],
|
||||
', '.join(
|
||||
('~~{}~~' if i == idx else '{}').format(n)
|
||||
for (i, n) in enumerate(results)
|
||||
)
|
||||
)
|
||||
|
||||
fitd = FITD_RE.search(content)
|
||||
if fitd:
|
||||
rating = int(fitd.group('rating'))
|
||||
if rating == 0:
|
||||
results = roll(2, 6)
|
||||
idx = results.index(max(results))
|
||||
value = min(results)
|
||||
crit = False
|
||||
else:
|
||||
results = roll(rating, 6)
|
||||
idx = -1
|
||||
value = max(results)
|
||||
crit = results.count(6) >= 2
|
||||
if crit:
|
||||
outcome = 'Crit'
|
||||
elif value == 6:
|
||||
outcome = 'Full success'
|
||||
elif value >= 4:
|
||||
outcome = 'Mixed success'
|
||||
else:
|
||||
outcome = 'Failure'
|
||||
return '**{}**\n**{}**\nRolls: {}'.format(
|
||||
outcome,
|
||||
"6, 6" if crit else value,
|
||||
', '.join(
|
||||
('~~{}~~' if i == idx else '{}').format(n)
|
||||
for (i, n) in enumerate(results)
|
||||
)
|
||||
)
|
||||
|
||||
sbr = SBR_RE.search(content)
|
||||
if sbr:
|
||||
pool = int(sbr.group('pool'))
|
||||
risk = sbr.group('risk')
|
||||
remove = 2 if risk == 'dang' else (1 if risk == 'risk' else 0)
|
||||
if remove >= pool:
|
||||
result = roll(1, 10)[0]
|
||||
return '**{}**\n{}'.format(
|
||||
'Success at a cost' if result == 10 else 'Failure',
|
||||
result,
|
||||
)
|
||||
results = roll(pool, 10)
|
||||
indices = list(range(pool))
|
||||
indices.sort(key=lambda i: results[i], reverse=True)
|
||||
indices = indices[:remove]
|
||||
best = max(n for (i, n) in enumerate(results) if i not in indices)
|
||||
if best == 10:
|
||||
outcome = 'Critical success'
|
||||
elif best >= 8:
|
||||
outcome = 'Success'
|
||||
elif best >= 6:
|
||||
outcome = 'Success at a cost'
|
||||
elif best >= 2:
|
||||
outcome = 'Failure'
|
||||
else:
|
||||
outcome = 'Critical failure'
|
||||
return '**{}**\n**{}**\nRolls: {}'.format(
|
||||
outcome,
|
||||
best,
|
||||
', '.join(
|
||||
('~~{}~~' if i in indices else '{}').format(n)
|
||||
for (i, n) in enumerate(results)
|
||||
)
|
||||
)
|
||||
|
||||
return USAGE
|
||||
|
||||
class RollcakeHandler(object):
|
||||
'''
|
||||
A Zulip bot to roll RPG dice.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return USAGE
|
||||
|
||||
def handle_message(self, message, bot_handler):
|
||||
bot_handler.send_reply(message, handle_roll(message['content']))
|
||||
|
||||
handler_class = RollcakeHandler
|
Loading…
Reference in a new issue