Shadowrun: Respecting Limits

Author
Tim Adler
Publishing date
September 24, 2024
Language
English

Introduction

Over a year ago, I wrote a series of blog posts about optimal edge use in the pen-and-paper roleplaying-game Shadowrun. Since then, a couple of things have happened, and I finally found the time to sum them up in this new post. I won’t go into the details about what Shadowrun is and what edge and things like that are. If you stumbled over this blog post, I would highly recommend having a look at the intro blog post of the original series.

When I told my Shadowrun party about my computations, they were very interested (maybe even excited). However, it became quickly apparent that my decision to ignore limits in my earlier computations was a bad idea. So I decided to think a bit more about the math and incorporate limits into my formulas.

As it turned out the math was rather straightforward, but I ran into trouble with the visualization. In fact, incorporating the limit into the decision meant that instead of only two quantities (the dice pool and the edge attribute), we now had three quantities (dice pool, edge, and limit), and plotting 3D things is hard (at least for me). I gave it a try, the result of which you can find below, but it did not pass the usability test in the form of my Shadowrun party. The graph just was not intuitive enough to interpret at a glance while actually playing the game. There are multiple options for how I could have dealt with this situation:

  1. Tell my friends to deal with it
  2. Think more about good data visualization
  3. Other nearby options
  4. Write a discord bot

Obviously, I went for option 4. After all, we play remotely using discord. I was interested to learn how discord bots work anyways, so this was an excellent excuse 😀

In this post, I will quickly go over the math for incorporating limits into the optimal decision. Afterward, I will show my failed visualization attempt and talk a bit about the problems. Lastly, I will showcase the discord bot and how I deployed it. You can find its code on GitHub.

Exact expectation value

The term ‘limit’ in Shadowrun means that you cannot have more successes on a roll than this limit, i.e., if you roll eight successes, but your limit is seven, only seven successes count. So far, so obvious. Furthermore, the edge option breaking the limits (sticking to the theme of its name) removes all limits. That’s great news for us because it means we don’t need to touch the formula at all. This only leaves the formula of the expected value in the second chance case.

In the original blog post, we found out that the dice role can be represented by a binomial distribution with probability p=59p=\frac59. The other key observation is that we can think of a limit as a minimum operation. If we denote the limit by ll, the dice pool by nn, and the number of successes by kk, then we are interested in the expected value of min(k,l)\min(k,l) over kk given nn. We can directly compute that via:

ESC[min(k,l)n]=k=0nmin(k,l)pSC(kn)=k=0lkpSC(kn)+k=l+1nlpSC(kn)=k=0lkpSC(kn)+l(1FSC(l))(1)=k=0nkpSC(kn)k=l+1n(kl)pSC(kn)=ESC[kn]k=l+1n(kl)pSC(kn)=:ESC[kn]r(n,l)(2)\begin{aligned}\mathbb{E}_\text{SC}[\min(k, l) \mid n] & = \sum_{k=0}^n \min(k, l) \cdot p_\text{SC}(k \mid n)\\& = \sum_{k=0}^l k \cdot p_\text{SC}(k \mid n) + \sum_{k=l+1}^n l \cdot p_\text{SC}(k \mid n)\\& = \sum_{k=0}^l k \cdot p_\text{SC}(k \mid n) + l \cdot (1 - F_\text{SC}(l))\hspace{10em} (1)\\ & = \sum_{k=0}^n k \cdot p_\text{SC}(k \mid n) - \sum_{k=l+1}^n (k - l) \cdot p_\text{SC}(k \mid n)\\ & = \mathbb{E}_\text{SC}[k \mid n] - \sum_{k=l+1}^n (k - l) \cdot p_\text{SC}(k \mid n)\\ & =: \mathbb{E}_\text{SC}[k \mid n] - r(n, l)\hspace{16.45em}(2) \end{aligned}

pSCp_\text{SC} denotes the probability mass function of the second chance process, which is just the binomial distribution mentioned above. I considered two massaged forms of the above equation. In (1), we would make use of the cumulative distribution function FSCF_\text{SC} of the distribution together with a sum which is part of the expectation value without the minimum. However, as there is no neat formula for the cumulative distribution function, I decided to go for representation (2), which modifies the unrestricted (or unlimited - pun intended) expected value (which has a closed-form solution) by a ‘residue’ rr.

To decide which option is better, we now only need to know if EBL[kn+e]\mathbb{E}_\text{BL}[k \mid n +e] is larger than ESC[min(k,l)n]\mathbb{E}_\text{SC}[\min(k,l) \mid n] or not (where ee denotes the edge attribute). This decision depends on the three quantities dice pool nn, edge attribute ee, and limit ll. Let’s see how I tried to visualize the decision boundary below.

Visualization attempt

Plots are 2D. So you have to get ‘creative’ if you want to visualize relationships between more than two variables. However, there are a couple of widely used options. One is color, which, of course, leads to trouble for people with color perception deficiencies. Nevertheless, it’s the route that I chose.

The idea was to keep ll fixed and plot the decision boundary having nn on the x-axis and ee on the y-axis. Then I could incorporate varying ll via the color of the decision boundary.

The result of this thought process can be seen below. There is also an interactive version where you can hide decision boundaries in which you are currently uninterested.

The inadequate visualization chart described in the main text.
The inadequate visualization chart described in the main text.

Even though I was happy with the visualization, it failed the practice test. In each and every session, there were questions about how to read the chart. There were just too many lines that, unfortunately, overlapped quite a bit, the color palette was also suboptimal, and the grid was suboptimal to track down your current situation (i.e., it was hard to find the intersection between your horizontal ee line and your vertical nn line keeping the limit in mind).

After a while, I accepted defeat and decided to outsource the complete mental burden to a discord bot.

Discord Bot

The math behind the decision is rather straightforward. So, the actual functional part of the script was easy, such that I could focus on surrounding parts like registering discord apps, talking to the API, and deploying the bot.

The discord developer portal was a very useful resource. Registering an app is explained there. Furthermore, there is a python wrapper for the API such that the main script of the bot consists of the following thirty-something lines:

import os

import discord
from dotenv import find_dotenv, load_dotenv

from optimal_edge.util import get_response

load_dotenv(find_dotenv())


def main():
    intents = discord.Intents.default()
    client = discord.Client(intents=intents)
    tree = discord.app_commands.CommandTree(client)

    @tree.command(
        name="optimaledge",
        description="Reports whether Breaking the Limit or Second Chance is superior.",
    )
    async def optimal_edge(interaction, pool: int, edge: int, limit: int):
        text = get_response(pool=pool, edge=edge, limit=limit)
        await interaction.response.send_message(text)

    @client.event
    async def on_ready():
        """Announce slash-commands"""
        await tree.sync()
        print("Ready!")

    client.run(os.environ["DISCORD_TOKEN"])


if __name__ == "__main__":
    main()
Entry point of the optimal edge discord bot (location: src/optimal_edge/main.py)

The bot needs an authentification token. I chose to use the python-dotenv package to expose the token as an environment variable. In other words, load_dotenv(find_dotenv()) together with a .env-file ensures that the environment variable DISCORD_TOKEN is set. Alternatively, you can set the environment variable manually.

If you are interested in the bot, you can find the complete code on GitHub.

As I don’t have a dedicated server at home, I deployed the bot on my uberspace instance. Uberspace uses supervisord as a process control system that can take care to restart the bot if it should crash. Also, the supervisord config file can be used to export environment variables which we can use to source the authentification token.

Recently, I started exploring ansible to deploy services on my uberspace. The role that I use (without secrets like the discord token) can be found in the bot repository.

The ansible tasks (ansible_playbook/tasks/main.yml) can be found in the following snippet:

- name: Install sr_optimal_edge_bot package
  pip:
    virtualenv: "{{ virtualenv_path }}"
    name: "{{ pip_name }}"
    virtualenv_command: python3.9 -m venv
    state: latest
  notify:
    - Restart sr_optimal_edge_bot service

- name: Check if symlink to sr_optimal_edge_bot executable exists
  stat:
    path: "{{ executable_path }}"
  register: stat_sr_optimal_edge_bot

- name: Symlink sr_optimal_edge_bot executable to bin
  file:
    src: "{{ virtualenv_path }}/bin/{{ executable }}"
    path: "{{ executable_path }}"
    state: link
  when: not stat_sr_optimal_edge_bot.stat.exists
  notify:
    - Restart sr_optimal_edge_bot service

- name: Check if sr_optimal_edge_bot service exists
  stat:
    path: "{{ service_file_location }}"
  register: stat_sr_optimal_edge_bot_service

- name: Install supervisor.d service file for sr_optimal_edge_bot
  template:
    src: templates/sr_optimal_edge_bot.ini.j2
    dest: "{{ service_file_location }}"
  notify:
    - Reread supervisor.d
    - Restart sr_optimal_edge_bot service
Tasks of the ansible role (location: ansible_playbook/tasks/main.yml)

Ansible performs five tasks:

  1. It installs the bot in a virtual environment (creating the environment if it does not exist)
  2. It checks if the bot’s executable has already been symlinked to ~/bin
  3. If not, it symlinks the executable to ~/bin
  4. It checks if the supervisord config file exists
  5. If not, it copies (and fills) the supervisord template.

Some of the tasks trigger so-called handlers because they make a restart of the service (or service discovery) necessary. The handlers are rather straightforward and can be found in ansible_playbook/handlers/main.yml.

After successful deployment, we only need to invite the bot to our discord instance (or guild), which is handled via invite links. Then we are rewarded with these beautiful interactions:

The slash command with tab completion in the discord chat.
The slash command with tab completion in the discord chat.
Output of the slash command if second chance is superior.
Output of the slash command if second chance is superior.
Output of the slash command if breaking the limit is superior.
Output of the slash command if breaking the limit is superior.

When I started this project, I erroneously suspected that I would spend most of my time on the math. While this turned out to be wrong, I am still very happy how this project progressed and escalated toward the development of a discord bot 🙂

I hope you found this post interesting and/or helpful. If you would like to play around with the bot, please reach out to me for an invite link. As uberspace computing resources are limited, I would rather not distribute the invite indiscriminately. Of course, you can always clone the bot’s repository and run it yourself.

If you have any comments, questions, or feedback, I would be excited to hear about it!

Privacy notice