add a !reaction dice roll for a very simple oracle-style vibe check

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
This commit is contained in:
Brian S. Stephan 2025-02-24 16:39:06 -06:00
parent 6d8ba18380
commit 3100efd4a9
Signed by: bss
GPG Key ID: 3DE06D3180895FCB
4 changed files with 121 additions and 2 deletions

View File

@ -4,10 +4,9 @@ import random
import re
from django.conf import settings
from irc.client import NickMask
from dice.lib import cypher_roll
from dice.lib import cypher_roll, reaction_roll
from dice.roller import DiceRoller
from ircbot.lib import Plugin
@ -15,6 +14,7 @@ logger = logging.getLogger(__name__)
CYPHER_ROLL_REGEX = r'((?P<type>A|T)(?P<difficulty>\d+))?(?P<mods>(?:\s*(-|\+)\d+)*)\s*(?P<comment>.*)?'
CYPHER_COMMAND_REGEX = r'^!cypher\s+(' + CYPHER_ROLL_REGEX + ')'
REACTION_COMMAND_REGEX = r'^!reaction$'
class Dice(Plugin):
@ -34,6 +34,8 @@ class Dice(Plugin):
self.handle_roll, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!random\s+(.*)$',
self.handle_random, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], REACTION_COMMAND_REGEX,
self.handle_reaction_roll, -20)
super(Dice, self).start()
@ -41,6 +43,8 @@ class Dice(Plugin):
"""Tear down handlers."""
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_roll)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_random)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_cypher_roll)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_reaction_roll)
super(Dice, self).stop()
@ -102,6 +106,20 @@ class Dice(Plugin):
reply = "{0:s}".format(choice)
return self.bot.reply(event, reply)
def handle_reaction_roll(self, connection, event, match):
"""Handle the !reaction roll."""
nick = NickMask(event.source).nick
roll, label, summary = reaction_roll()
if summary in ('--', '-'):
result_str = f"4{label}"
elif summary == '~':
result_str = f"9{label}"
else:
result_str = f"9{label}"
return self.bot.reply(event, f"{nick}: the current disposition is: {result_str} 14({roll})")
def handle_roll(self, connection, event, match):
"""Handle the !roll command which covers most common dice stuff."""
nick = NickMask(event.source).nick

View File

@ -40,3 +40,32 @@ def cypher_roll(difficulty=None, mods=0, is_attack=False):
beats = (roll // 3) - (numexpr.evaluate(mods).item() if mods else 0)
beats = 0 if beats < 0 else beats
return (roll, beats, difficulty <= beats if difficulty else None, effect)
def reaction_roll():
"""Make a reaction roll, which gives some oracle-like randomness to uncertain situations (for solo play or similar).
Returns:
tuple of:
- the result on the d20
- the label for the result
- a summary of the result ('++', '+', '~', '-', '--') (best to worst)
"""
roll = rand.randint(1, 20)
if 1 == roll:
label = 'VERY negative'
summary = '--'
elif 2 <= roll <= 6:
label = 'negative'
summary = '-'
elif 7 <= roll <= 14:
label = 'positive, with complications'
summary = '~'
elif 15 <= roll <= 19:
label = 'positive'
summary = '+'
else:
label = 'VERY positive'
summary = '++'
return (roll, label, summary)

View File

@ -80,3 +80,56 @@ class MarkovTestCase(TestCase):
mock_event,
'test: unmodded attempt beats a difficulty 3 task. 14(d20=9)'
)
def test_reaction_roll_strings(self):
"""Simulate incoming reaction requests."""
mock_event = mock.MagicMock()
mock_event.source = 'test!test@test'
mock_event.target = '#test'
mock_event.recursing = False
# good and very good are bold green
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=18):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is: 9positive 14(18)'
)
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=20):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is: 9VERY positive 14(20)'
)
# decent is green
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=10):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is: 9positive, with complications 14(10)'
)
# bad and very bad are bold red
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=4):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is: 4negative 14(4)'
)
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=1):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is: 4VERY negative 14(1)'
)

View File

@ -85,3 +85,22 @@ class DiceLibTestCase(TestCase):
with mock.patch('random.SystemRandom.randint', return_value=10):
result = dice.lib.cypher_roll(mods='2')
self.assertEqual(result, (10, 1, None, None))
def test_reaction_roll(self):
"""Roll possible reactions."""
with mock.patch('random.SystemRandom.randint', return_value=1):
self.assertEqual(dice.lib.reaction_roll(), (1, 'VERY negative', '--'))
with mock.patch('random.SystemRandom.randint', return_value=2):
self.assertEqual(dice.lib.reaction_roll(), (2, 'negative', '-'))
with mock.patch('random.SystemRandom.randint', return_value=6):
self.assertEqual(dice.lib.reaction_roll(), (6, 'negative', '-'))
with mock.patch('random.SystemRandom.randint', return_value=7):
self.assertEqual(dice.lib.reaction_roll(), (7, 'positive, with complications', '~'))
with mock.patch('random.SystemRandom.randint', return_value=14):
self.assertEqual(dice.lib.reaction_roll(), (14, 'positive, with complications', '~'))
with mock.patch('random.SystemRandom.randint', return_value=15):
self.assertEqual(dice.lib.reaction_roll(), (15, 'positive', '+'))
with mock.patch('random.SystemRandom.randint', return_value=19):
self.assertEqual(dice.lib.reaction_roll(), (19, 'positive', '+'))
with mock.patch('random.SystemRandom.randint', return_value=20):
self.assertEqual(dice.lib.reaction_roll(), (20, 'VERY positive', '++'))