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:
- Tell my friends to deal with it
- Think more about good data visualization
- Other nearby options
- 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 . The other key observation is that we can think of a limit as a minimum operation. If we denote the limit by , the dice pool by , and the number of successes by , then we are interested in the expected value of over given . We can directly compute that via:
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 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’ .
To decide which option is better, we now only need to know if is larger than or not (where denotes the edge attribute). This decision depends on the three quantities dice pool , edge attribute , and limit . 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 fixed and plot the decision boundary having on the x-axis and on the y-axis. Then I could incorporate varying 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.
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 line and your vertical 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()
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
ansible_playbook/tasks/main.yml
)Ansible performs five tasks:
- It installs the bot in a virtual environment (creating the environment if it does not exist)
- It checks if the bot’s executable has already been symlinked to
~/bin
- If not, it symlinks the executable to
~/bin
- It checks if the supervisord config file exists
- 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:
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!