dice: cypher GM intrusions should not be auto-fails

this also has some code reorg around IRC styles

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
This commit is contained in:
2026-07-24 11:30:44 -05:00
parent e0ae5335f4
commit 59d462fcd0
3 changed files with 54 additions and 47 deletions
+35 -43
View File
@@ -10,7 +10,7 @@ rand = random.SystemRandom()
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]]:
-> Tuple[int, int, Optional[bool], Optional[str], Optional[str], int, Optional[int]]:
"""Make a Cypher System roll.
Args:
@@ -34,10 +34,8 @@ def cypher_roll(difficulty: Optional[int] = None, mod: int = 0, is_attack: bool
else:
net_difficulty = None
if roll == 1:
return (roll, None, False if difficulty else None, None, 'a GM intrusion', result_lvl, net_difficulty)
effect = None
negative_effect = None
if roll == 17 and is_attack:
effect = '+1 damage'
elif roll == 18 and is_attack:
@@ -46,19 +44,34 @@ def cypher_roll(difficulty: Optional[int] = None, mod: int = 0, is_attack: bool
effect = 'a minor effect'
elif roll == 20:
effect = 'a MAJOR EFFECT'
elif roll == 1:
negative_effect = 'a GM intrusion'
# if we know the difficulty, the mod 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 = result_lvl - mod
beats = 0 if beats < 0 else beats
return (roll, beats, difficulty <= beats if difficulty else None, effect, None, result_lvl, net_difficulty)
return (roll, beats, difficulty <= beats if difficulty else None, effect, negative_effect,
result_lvl, net_difficulty)
def cypher_result(difficulty: Optional[int] = None, mod: int = 0, is_attack: bool = False, comment: str = '',
stat: str = 'Unknown', ircify: bool = False) \
-> Tuple[int, Optional[int], Optional[bool], Optional[str], Optional[str], int, Optional[int], str]:
-> Tuple[int, int, Optional[bool], Optional[str], Optional[str], int, Optional[int], str]:
"""Turn the results of a Cypher System roll into a display string."""
# prep some IRC stuff
if ircify:
good_style_prefix = '9'
bad_style_prefix = '4'
comment_style_prefix = '14'
style_suffix = ''
else:
good_style_prefix = ''
bad_style_prefix = ''
comment_style_prefix = ''
style_suffix = ''
# tweak the comment to show task/attack in cases of empty string
if comment:
display_comment = f"{comment} {stat} roll"
@@ -69,33 +82,22 @@ def cypher_result(difficulty: Optional[int] = None, mod: int = 0, is_attack: boo
result, beats, success, effect, neg_effect, result_lvl, diff_level = cypher_roll(
difficulty=difficulty, mod=mod, is_attack=is_attack,
)
if success is not None:
# we know the difficulty so we should always know the success/failure
# TODO: less nested ifs
if success:
if ircify:
style_prefix = '9'
style_suffix = ''
else:
style_prefix = ''
style_suffix = ''
if effect:
result_str = f"{style_prefix}succeeded, with {effect}!{style_suffix}"
result_str = f"{good_style_prefix}succeeded, with {effect}!{style_suffix}"
elif neg_effect:
result_str = (f"{good_style_prefix}succeeded{style_suffix}, "
f"{bad_style_prefix}with {neg_effect}!{style_suffix}")
else:
result_str = f"{style_prefix}succeeded!{style_suffix}"
result_str = f"{good_style_prefix}succeeded!{style_suffix}"
else:
if ircify:
style_prefix = '4'
style_suffix = ''
else:
style_prefix = ''
style_suffix = ''
if neg_effect:
result_str = f"{style_prefix}failed, with {neg_effect}!{style_suffix}"
result_str = f"{bad_style_prefix}failed, with {neg_effect}!{style_suffix}"
else:
result_str = f"{style_prefix}failed.{style_suffix}"
result_str = f"{bad_style_prefix}failed.{style_suffix}"
# show the adjusted difficulty
if mod > 0:
@@ -105,23 +107,16 @@ def cypher_result(difficulty: Optional[int] = None, mod: int = 0, is_attack: boo
else:
modded_diff = str(difficulty)
if ircify:
detail_str = f"14(result {result_lvl} (d20={result}) vs. difficulty {diff_level} ({modded_diff}))"
else:
detail_str = f"(result {result_lvl} (d20={result}) vs. difficulty {diff_level} ({modded_diff}))"
detail_str = (f"{comment_style_prefix}(result {result_lvl} (d20={result}) vs. difficulty {diff_level} "
f"({modded_diff})){style_suffix}")
else:
# we don't know the difficulty, so don't know success or not
if beats is not None:
if effect:
result_str = f"beats a difficulty {beats} task, with {effect}!"
else:
result_str = f"beats a difficulty {beats} task."
if effect:
result_str = f"beats a difficulty {beats} task, {good_style_prefix}with {effect}!{style_suffix}"
elif neg_effect:
result_str = f"beats a difficulty {beats} task, {bad_style_prefix}with {neg_effect}!{style_suffix}"
else:
# this can only happen on an intrusion
if ircify:
result_str = f"4beats nothing, with {neg_effect}!"
else:
result_str = f"beats nothing, with {neg_effect}!"
result_str = f"beats a difficulty {beats} task."
# show the adjusted difficulty
if mod > 0:
@@ -131,10 +126,7 @@ def cypher_result(difficulty: Optional[int] = None, mod: int = 0, is_attack: boo
else:
modded_diff = ""
if ircify:
detail_str = f"14(result {result_lvl}{modded_diff} (d20={result}))"
else:
detail_str = f"(result {result_lvl}{modded_diff} (d20={result}))"
detail_str = f"{comment_style_prefix}(result {result_lvl}{modded_diff} (d20={result})){style_suffix}"
return (result, beats, success, effect, neg_effect, result_lvl, diff_level,
f"{display_comment} {result_str} {detail_str}")
+17 -3
View File
@@ -29,7 +29,7 @@ class CypherRollTestCase(TestCase):
"""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))
self.assertEqual(result, (1, 0, False, None, 'a GM intrusion', 0, 1))
def test_attack_17(self):
"""Test an attack with bonus damage."""
@@ -89,7 +89,7 @@ class CypherRollTestCase(TestCase):
with mock.patch('random.SystemRandom.randint', return_value=1):
result = dice.lib.cypher_roll()
self.assertEqual(result, (1, None, None, None, 'a GM intrusion', 0, None))
self.assertEqual(result, (1, 0, None, None, 'a GM intrusion', 0, None))
def test_unknown_difficulty(self):
"""Test general "don't know the difficulty" kind of checks."""
@@ -102,6 +102,12 @@ class CypherRollTestCase(TestCase):
result = dice.lib.cypher_roll(mod=2)
self.assertEqual(result, (10, 1, None, None, None, 3, None))
def test_success_despite_intrusion(self):
"""Test that successes can occur even if a 1 is rolled."""
with mock.patch('random.SystemRandom.randint', return_value=1):
result = dice.lib.cypher_roll(difficulty=2, mod=-2)
self.assertEqual(result, (1, 2, True, None, "a GM intrusion", 0, 0))
class DiceLibTestCase(TestCase):
"""Test that a variety of dice rolls work as expected."""
@@ -155,7 +161,7 @@ class CypherRollStringTestCase(TestCase):
with mock.patch('random.SystemRandom.randint', return_value=1):
_, _, _, _, _, _, _, result = dice.lib.cypher_result(mod=0, stat="Might",
comment="Test")
self.assertEqual(result, "Test Might roll beats nothing, with a GM intrusion! (result 0 (d20=1))")
self.assertEqual(result, "Test Might roll beats a difficulty 0 task, with a GM intrusion! (result 0 (d20=1))")
def test_unknown_difficulty_effect(self):
"""Test the output when the difficulty is unknown."""
@@ -163,3 +169,11 @@ class CypherRollStringTestCase(TestCase):
_, _, _, _, _, _, _, result = dice.lib.cypher_result(mod=0, stat="Might",
comment="Test")
self.assertEqual(result, "Test Might roll beats a difficulty 6 task, with a MAJOR EFFECT! (result 6 (d20=20))")
def test_success_despite_intrusion(self):
"""Test that successes can occur even if a 1 is rolled."""
with mock.patch('random.SystemRandom.randint', return_value=1):
_, _, _, _, _, _, _, result = dice.lib.cypher_result(difficulty=2, mod=-2, stat="Might",
comment="Test")
self.assertEqual(result,
"Test Might roll succeeded, with a GM intrusion! (result 0 (d20=1) vs. difficulty 0 (2-2))")
+2 -1
View File
@@ -83,4 +83,5 @@ class DiceRollerTestCase(TestCase):
"""Roll a Cypher System roll without difficulty, just mods."""
with mock.patch('random.SystemRandom.randint', return_value=1):
result = self.roller.do_roll('-1 might attack')
self.assertEqual(result, 'attack might roll beats nothing, with a GM intrusion! (result 0+1 (d20=1))')
self.assertEqual(result, 'attack might roll beats a difficulty 1 task, '
'with a GM intrusion! (result 0+1 (d20=1))')