add a Cypher System dice rolling library method

not available to the bot, yet, but that's next

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
This commit is contained in:
Brian S. Stephan 2024-11-07 12:23:32 -06:00
parent 994f9ef50b
commit ac16ec99e5
Signed by: bss
GPG Key ID: 3DE06D3180895FCB
5 changed files with 128 additions and 2 deletions

41
dice/lib.py Normal file
View File

@ -0,0 +1,41 @@
"""Dice rolling operations (outside of the lex/yacc roller)."""
import random
import numexpr
rand = random.SystemRandom()
def cypher_roll(difficulty=None, mods=0):
"""Make a Cypher System roll.
Args:
difficulty: the original difficulty to beat; if provided, success or failure is indicated in the results
mods: eases(-) and hindrances(+) to apply to the check, as a string (e.g. '-3+1')
Returns:
tuple of:
- the result on the d20
- the highest difficulty beaten
- if the difficulty is known, if the target was beat
- miscellaneous effects
"""
roll = rand.randint(1, 20)
if roll == 1:
return (roll, None, False if difficulty else None, 'a GM intrusion')
effect = None
if roll == 17:
effect = '+1 damage'
elif roll == 18:
effect = '+2 damage'
elif roll == 19:
effect = 'a minor effect'
elif roll == 20:
effect = 'a MAJOR EFFECT'
# if we know the difficulty, the mods would adjust the difficulty, but for the case where we don't,
# and maybe just in general, it's easier to modify the difficulty that the roll beats, so we flip the logic
# if incoming eases are a negative number, they should add to the difficulty the roll beats
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)

View File

@ -11,8 +11,8 @@ authors = [
{name = "Brian S. Stephan", email = "bss@incorporeal.org"}, {name = "Brian S. Stephan", email = "bss@incorporeal.org"},
] ]
requires-python = ">=3.10,<3.12" requires-python = ">=3.10,<3.12"
dependencies = ["Django<5.1", "django-bootstrap3", "django-extensions", "djangorestframework", "irc", "parsedatetime", "ply", dependencies = ["Django<5.1", "django-bootstrap3", "django-extensions", "djangorestframework", "irc", "numexpr",
"python-dateutil", "python-mpd2", "pytz", "zalgo-text"] "parsedatetime", "ply", "python-dateutil", "python-mpd2", "pytz", "zalgo-text"]
dynamic = ["version"] dynamic = ["version"]
classifiers = [ classifiers = [
"Framework :: Django", "Framework :: Django",

View File

@ -152,6 +152,10 @@ more-itertools==10.5.0
# jaraco-functools # jaraco-functools
# jaraco-stream # jaraco-stream
# jaraco-text # jaraco-text
numexpr==2.10.1
# via dr.botzo (pyproject.toml)
numpy==2.1.3
# via numexpr
packaging==24.1 packaging==24.1
# via # via
# build # build

View File

@ -49,6 +49,10 @@ more-itertools==10.5.0
# jaraco-functools # jaraco-functools
# jaraco-stream # jaraco-stream
# jaraco-text # jaraco-text
numexpr==2.10.1
# via dr.botzo (pyproject.toml)
numpy==2.1.3
# via numexpr
parsedatetime==2.6 parsedatetime==2.6
# via dr.botzo (pyproject.toml) # via dr.botzo (pyproject.toml)
ply==3.11 ply==3.11

77
tests/test_dice_lib.py Normal file
View File

@ -0,0 +1,77 @@
"""Tests for dice operations."""
from unittest import mock
from django.test import TestCase
import dice.lib
class DiceLibTestCase(TestCase):
"""Test that a variety of dice rolls work as expected."""
def test_cypher_rolls(self):
"""Roll a variety of Cypher System rolls."""
# simple task, simple check
with mock.patch('random.SystemRandom.randint', return_value=5):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (5, 1, True, None))
# simple failure
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (2, 0, False, None))
# rolled a 1
with mock.patch('random.SystemRandom.randint', return_value=1):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (1, None, False, 'a GM intrusion'))
# rolled a 17
with mock.patch('random.SystemRandom.randint', return_value=17):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (17, 5, True, '+1 damage'))
# rolled a 18
with mock.patch('random.SystemRandom.randint', return_value=18):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (18, 6, True, '+2 damage'))
# rolled a 19
with mock.patch('random.SystemRandom.randint', return_value=19):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (19, 6, True, 'a minor effect'))
# rolled a 20
with mock.patch('random.SystemRandom.randint', return_value=20):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (20, 6, True, 'a MAJOR EFFECT'))
# mods affect the result of what the roll beats
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(difficulty=1, mods='-5')
self.assertEqual(result, (2, 5, True, None))
# complex mods
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(difficulty=3, mods='+1-4')
self.assertEqual(result, (2, 3, True, None))
# complex mods
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(difficulty=3, mods='-4+1')
self.assertEqual(result, (2, 3, True, None))
# ...even without a difficulty known
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(mods='-5')
self.assertEqual(result, (2, 5, None, None))
# general "don't know the difficulty" kind of check
with mock.patch('random.SystemRandom.randint', return_value=10):
result = dice.lib.cypher_roll(mods='-2')
self.assertEqual(result, (10, 5, None, None))
# general "don't know the difficulty" kind of check in the other direction
with mock.patch('random.SystemRandom.randint', return_value=10):
result = dice.lib.cypher_roll(mods='2')
self.assertEqual(result, (10, 1, None, None))