Diary 2019-02-27

By Max Woerner Chase

Before anything else, here is some code that I refuse to work with in any remotely professional capacity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import inspect
import sys


class TailCall(BaseException):
    def __init__(self, function, args, kwargs, exits):
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.exits = exits


def process_exits(exits, exc_type=None, exc_value=None, traceback=None):
    for exit_group in reversed(exits):
        for exit in exit_group:
            if exit(exc_type, exc_value, traceback):
                exc_type = exc_value = traceback = None
    return exc_type is not None


class tail_call:

    __thing = None

    def __init__(self, thing):
        self._thing = thing

    @property
    def _thing(self):
        return self.__thing

    @_thing.setter
    def _thing(self, value):
        self.__thing = value
        self.__doc__ = value.__doc__

    def __get__(self, instance, owner):
        return tail_call(
            inspect.getattr_static(self._thing, "__get__")(self._thing, instance, owner)
        )

    def __call__(self, *args, **kwargs):
        frame = sys._getframe()
        function = self._thing
        if (
            frame.f_back
            and frame.f_back.f_back
            and frame.f_back.f_back.f_code == frame.f_code
        ):
            raise TailCall(function, args, kwargs, [])
        exits = []
        raised = False
        try:
            while True:
                try:
                    return function(*args, **kwargs)
                except TailCall as tc:
                    function = tc.function
                    args = tc.args
                    kwargs = tc.kwargs
                    exits.append(tc.exits)
        except:
            raised = True
            if process_exits(exits, *sys.exc_info()):
                raise
        finally:
            if not raised:
                process_exits(exits)

    def call(self, *args, **kwargs):
        return self._thing(*args, **kwargs)

    def __enter__(self):
        return self._thing.__enter__()

    def __exit__(self, exc_type, exc_value, traceback):
        exit_func = self._thing.__exit__
        if exc_type is not TailCall:
            return exit_func(exc_type, exc_value, traceback)
        exc_value.exits.append(exit_func)

Anyway, I'm going to try to go into a little more detail on the dice-rolling stuff. It might be useful to people rolling their own.

First off, one of the most important things to me in writing code to simulate die rolls, is making sure the rolls are handled transparently. While it's possible to consider simple rolls in terms of binomial distributions, turning rolls for a tabletop game into a black box doesn't really serve any purpose. My feeling on this is, we want to have as easy of a time as possible checking the logic; ideally, the roller should surface all of the information required to replicate the results by hand, given the random inputs used.

My next priority was presenting this information in a way that makes sense. To this end, I've got an object that effectively allows the construction of two parallel narratives. The first details what was rolled, and for what purpose. The second distills and interprets the results into high-level descriptions. Let's use the current maneuver roller:

>>> rolls.maneuver(10)  # Don't know if you can do this in normal play.
Rolling a maneuver die.
Rolled 3.
Rolling a maneuver die.
Rolled 3.
Rolling a maneuver die.
Rolled 2.
Rolling a maneuver die.
Rolled 2.
Rolling a maneuver die.
Rolled 5.
Rolling a maneuver die.
Rolled 6.
Rolling a maneuver die.
Rolled 6.
Rolling a maneuver die.
Rolled 5.
Rolling a maneuver die.
Rolled 3.
Rolling a maneuver die.
Rolled 6.
--------------------------------------------------------------------------------
Strike dice: 5.
Charge dice: 3.
Failed dice: 2.
Earned an Awesome Token.

There are several aspects to this:

I'm going to try to wrap things up this week, but I'm right now thinking about writing an interface using the cmd module to simplify things. The big thing I'd add over the existing functionality is a top-level command for rolling arbitrary-sized dice, for how Mythic wants you to choose randomly out of lists sometimes.

I'll try to get that put together, and then show off the code later. I promise it's less horrifying than the stuff up top.

(The joke in the summary is that the monospaced text is going way off to the right because it is long and cannot wrap.)