rollcake bot
This commit is contained in:
		
							parent
							
								
									02586f1d34
								
							
						
					
					
						commit
						86e4c583b4
					
				
					 1 changed files with 170 additions and 0 deletions
				
			
		
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue