Compare commits

...

5 Commits

Author SHA1 Message Date
6d8ba18380
cypher: distinguish between a task roll and an attack roll
attack = A, otherwise the same options as with a generic task (= T)

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
3b7c871a94
cypher: case insensitivity when matching task difficulties
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
fa4815153a
cypher: slightly better display of output with no difficulty or mods
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
cc9b110531
fix a display issue in the karma key score
this would probably only matter if adding a key manually that doesn't
have a score, but a fix is a fix

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
af4746fb90
allow Markov contexts to be hidden from the web
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
8 changed files with 90 additions and 21 deletions

View File

@ -13,7 +13,7 @@ from ircbot.lib import Plugin
logger = logging.getLogger(__name__)
CYPHER_ROLL_REGEX = r'(T(?P<difficulty>\d+))?(?P<mods>(?:\s*(-|\+)\d+)*)\s*(?P<comment>.*)?'
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 + ')'
@ -49,11 +49,12 @@ class Dice(Plugin):
nick = NickMask(event.source).nick
task = match.group(1)
task_group = re.search(CYPHER_ROLL_REGEX, task)
task_group = re.search(CYPHER_ROLL_REGEX, task, re.IGNORECASE)
difficulty = int(task_group.group('difficulty')) if task_group.group('difficulty') else None
mods = task_group.group('mods')
is_attack = True if task_group.group('type') and task_group.group('type').upper() == 'A' else False
comment = task_group.group('comment')
result, beats, success, effect = cypher_roll(difficulty=difficulty, mods=mods)
result, beats, success, effect = cypher_roll(difficulty=difficulty, mods=mods, is_attack=is_attack)
if success is not None:
if success:
@ -76,12 +77,13 @@ class Dice(Plugin):
# show the adjusted difficulty
detail_str = f"14(d20={result} vs. diff. {difficulty}{mods})"
else:
detail_str = f"14(d20={result} with {mods} levels)"
detail_str = f"14(d20={result}{f' with {mods} levels' if mods else ''})"
if comment:
return self.bot.reply(event, f"{nick}: {comment} {result_str} {detail_str}")
else:
return self.bot.reply(event, f"{nick}: your check {result_str} {detail_str}")
type_str = 'attack' if is_attack else 'check'
return self.bot.reply(event, f"{nick}: your {type_str} {result_str} {detail_str}")
def handle_random(self, connection, event, match):
"""Handle the !random command which picks an item from a list."""

View File

@ -6,12 +6,13 @@ import numexpr
rand = random.SystemRandom()
def cypher_roll(difficulty=None, mods=0):
def cypher_roll(difficulty=None, mods=0, is_attack=False):
"""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')
is_attack: if the roll is an attack (in which case the damage-only effects are included)
Returns:
tuple of:
- the result on the d20
@ -24,9 +25,9 @@ def cypher_roll(difficulty=None, mods=0):
return (roll, None, False if difficulty else None, 'a GM intrusion')
effect = None
if roll == 17:
if roll == 17 and is_attack:
effect = '+1 damage'
elif roll == 18:
elif roll == 18 and is_attack:
effect = '+2 damage'
elif roll == 19:
effect = 'a minor effect'

View File

@ -1,14 +1,13 @@
"""Karma logging models."""
from datetime import timedelta
import logging
from datetime import timedelta
import pytz
from irc.client import NickMask
from django.conf import settings
from django.db import models
from django.utils import timezone
from irc.client import NickMask
log = logging.getLogger('karma.models')
@ -44,12 +43,13 @@ class KarmaKey(models.Model):
objects = KarmaKeyManager()
def __str__(self):
"""String representation."""
"""Display the karma key and score."""
return "{0:s} ({1:d})".format(self.key, self.score())
def score(self):
"""Determine the score for this karma entry."""
return KarmaLogEntry.objects.filter(key=self).aggregate(models.Sum('delta'))['delta__sum']
score = KarmaLogEntry.objects.filter(key=self).aggregate(models.Sum('delta'))['delta__sum']
return score if score else 0
def rank(self):
"""Determine the rank of this karma entry relative to the others."""

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2024-11-14 15:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('markov', '0009_alter_markovcontext_id_alter_markovstate_id_and_more'),
]
operations = [
migrations.AddField(
model_name='markovcontext',
name='web_enabled',
field=models.BooleanField(default=True),
),
]

View File

@ -12,6 +12,7 @@ class MarkovContext(models.Model):
"""Define contexts for Markov chains."""
name = models.CharField(max_length=200, unique=True)
web_enabled = models.BooleanField(default=True)
def __str__(self):
"""Provide string representation."""

View File

@ -3,6 +3,7 @@ import json
import logging
import time
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from rest_framework.authentication import BasicAuthentication
@ -27,6 +28,8 @@ def context_index(request, context_id):
start_t = time.time()
context = get_object_or_404(MarkovContext, pk=context_id)
if not context.web_enabled:
raise PermissionDenied()
chain = " ".join(markovlib.generate_line(context))
end_t = time.time()

View File

@ -26,23 +26,57 @@ class MarkovTestCase(TestCase):
def test_cypher_roll_strings(self):
"""Simulate incoming Cypher System requests."""
mock_event = mock.MagicMock()
mock_event.arguments = ['!cypher T3']
mock_event.source = 'test!test@test'
mock_event.target = '#test'
mock_event.recursing = False
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, '!cypher T3')
# general task roll (no damage output on a 17)
mock_event.arguments = ['!cypher T3']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=17):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: your check 9succeeded, with +1 damage! 14(d20=17 vs. diff. 3)'
'test: your check 9succeeded! 14(d20=17 vs. diff. 3)'
)
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, '!cypher +1')
# general attack roll (incl. damage output on a 17)
mock_event.arguments = ['!cypher A3']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=17):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: your check beats a difficulty 4 task, with +1 damage! 14(d20=17 with +1 levels)'
'test: your attack 9succeeded, with +1 damage! 14(d20=17 vs. diff. 3)'
)
# general task roll, case insensitive
mock_event.arguments = ['!cypher t3']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=17):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: your check 9succeeded! 14(d20=17 vs. diff. 3)'
)
# unknown target roll
mock_event.arguments = ['!cypher +1']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=17):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: your check beats a difficulty 4 task. 14(d20=17 with +1 levels)'
)
# no mod or known difficulty
mock_event.arguments = ['!cypher unmodded attempt']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
with mock.patch('random.SystemRandom.randint', return_value=9):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: unmodded attempt beats a difficulty 3 task. 14(d20=9)'
)

View File

@ -26,15 +26,25 @@ class DiceLibTestCase(TestCase):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (1, None, False, 'a GM intrusion'))
# rolled a 17 on an attack
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'))
# rolled a 18 on an attack
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'))
# 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'))
self.assertEqual(result, (17, 5, True, None))
# 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'))
self.assertEqual(result, (18, 6, True, None))
# rolled a 19
with mock.patch('random.SystemRandom.randint', return_value=19):