cypher dice: some cleanups from my fork that I'm merging back

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
This commit is contained in:
2026-07-24 07:49:42 -05:00
parent eb325f98b0
commit 6fec81917e
2 changed files with 56 additions and 27 deletions
+12 -5
View File
@@ -1,10 +1,16 @@
"""Dice rolling operations (outside of the lex/yacc roller)."""
"""Dice rolling operations (outside of the lex/yacc roller).
SPDX-FileCopyrightText: © 2026 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
import random
from typing import Optional, Tuple
rand = random.SystemRandom()
def cypher_roll(difficulty: int = None, mod: int = 0, is_attack: bool = False):
def cypher_roll(difficulty: Optional[int] = None, mod: int = 0, is_attack: bool = False) \
-> Tuple[int, Optional[int], Optional[bool], Optional[str], Optional[str], int, Optional[int]]:
"""Make a Cypher System roll.
Args:
@@ -16,8 +22,8 @@ def cypher_roll(difficulty: int = None, mod: int = 0, is_attack: bool = False):
- the result on the d20
- the highest difficulty beaten
- if the difficulty is known, if the target was beat
- miscellaneous effects
- miscellaneous negative effects
- miscellaneous effects, if any
- miscellaneous negative effects, if any
- the level of the result on the d20
- the net difficulty level, if known
"""
@@ -57,6 +63,7 @@ def cypher_result(difficulty: int, modifier: int, is_attack: bool, stat: str, co
else:
display_comment = "attack" if is_attack else "task"
# roll it
result, beats, success, effect, neg_effect, result_lvl, diff_level = cypher_roll(
difficulty=difficulty, mod=modifier, is_attack=is_attack
)
@@ -94,7 +101,7 @@ def cypher_result(difficulty: int, modifier: int, is_attack: bool, stat: str, co
elif modifier < 0:
modded_diff = f"{difficulty}{modifier}"
else:
modded_diff = difficulty
modded_diff = str(difficulty)
if ircify:
detail_str = f"14(res. {result_lvl} (d20={result}) vs. {stat} difficulty {diff_level} ({modded_diff}))"
+44 -22
View File
@@ -1,4 +1,8 @@
"""Tests for dice operations."""
"""Tests for dice operations.
SPDX-FileCopyrightText: © 2026 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
from unittest import mock
from django.test import TestCase
@@ -6,57 +10,65 @@ from django.test import TestCase
import dice.lib
class DiceLibTestCase(TestCase):
"""Test that a variety of dice rolls work as expected."""
class CypherRollTestCase(TestCase):
"""Test that a variety of Cypher System dice rolls work as expected."""
def test_cypher_rolls(self):
"""Roll a variety of Cypher System rolls."""
# simple task, simple check
def test_simple_check(self):
"""Test a 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, None, 1, 1))
# simple failure
def test_simple_check_failure(self):
"""Test a 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, None, 0, 1))
# rolled a 1
def test_gm_intrusion(self):
"""Test rolling a 1."""
with mock.patch('random.SystemRandom.randint', return_value=1):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (1, None, False, None, 'a GM intrusion', 0, 1))
# rolled a 17 on an attack
def test_attack_17(self):
"""Test an attack with bonus damage."""
with mock.patch('random.SystemRandom.randint', return_value=17):
result = dice.lib.cypher_roll(difficulty=1, is_attack=True)
self.assertEqual(result, (17, 5, True, '+1 damage', None, 5, 1))
# rolled a 18 on an attack
def test_attack_18(self):
"""Test an attack with more bonus damage."""
with mock.patch('random.SystemRandom.randint', return_value=18):
result = dice.lib.cypher_roll(difficulty=1, is_attack=True)
self.assertEqual(result, (18, 6, True, '+2 damage', None, 6, 1))
# rolled a 17
def test_task_17(self):
"""Test a task 17 has no bonus damage."""
with mock.patch('random.SystemRandom.randint', return_value=17):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (17, 5, True, None, None, 5, 1))
# rolled a 18
def test_task_18(self):
"""Test a task 18 has no bonus damage."""
with mock.patch('random.SystemRandom.randint', return_value=18):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (18, 6, True, None, None, 6, 1))
# rolled a 19
def test_19(self):
"""Test a 19 has a minor effect."""
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', None, 6, 1))
# rolled a 20
def test_20(self):
"""Test a 20 has a minor effect."""
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', None, 6, 1))
# mods affect the result of what the roll beats
def test_modifiers_beat_rolls(self):
"""Test that 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, mod=-5)
self.assertEqual(result, (2, 5, True, None, None, 0, -4))
@@ -65,7 +77,8 @@ class DiceLibTestCase(TestCase):
result = dice.lib.cypher_roll(difficulty=3, mod=-3)
self.assertEqual(result, (2, 3, True, None, None, 0, 0))
# ...even without a difficulty known
def test_modifiers_beat_rolls_unknown_difficulty(self):
"""Test that mods affect the result of what the roll beats even without a difficulty known."""
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(mod=-5)
self.assertEqual(result, (2, 5, None, None, None, 0, None))
@@ -78,7 +91,8 @@ class DiceLibTestCase(TestCase):
result = dice.lib.cypher_roll()
self.assertEqual(result, (1, None, None, None, 'a GM intrusion', 0, None))
# general "don't know the difficulty" kind of check
def test_unknown_difficulty(self):
"""Test general "don't know the difficulty" kind of checks."""
with mock.patch('random.SystemRandom.randint', return_value=10):
result = dice.lib.cypher_roll(mod=-2)
self.assertEqual(result, (10, 5, None, None, None, 3, None))
@@ -88,11 +102,9 @@ class DiceLibTestCase(TestCase):
result = dice.lib.cypher_roll(mod=2)
self.assertEqual(result, (10, 1, None, None, None, 3, None))
def test_cypher_result(self):
"""Test the Cypher result string generation."""
with mock.patch('random.SystemRandom.randint', return_value=10):
result = dice.lib.cypher_result(3, 0, False, "Might", "")
self.assertEqual(result, "task succeeded! (res. 3 (d20=10) vs. Might difficulty 3 (3))")
class DiceLibTestCase(TestCase):
"""Test that a variety of dice rolls work as expected."""
def test_reaction_roll(self):
"""Roll possible reactions."""
@@ -112,3 +124,13 @@ class DiceLibTestCase(TestCase):
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', '++'))
class CypherRollStringTestCase(TestCase):
"""Test that Cypher System roll results are in the expected string representation."""
def test_simple_result(self):
"""Test the basics of string output."""
with mock.patch('random.SystemRandom.randint', return_value=10):
result = dice.lib.cypher_result(3, 0, False, "Might", "")
self.assertEqual(result, "task succeeded! (res. 3 (d20=10) vs. Might difficulty 3 (3))")