TTRPG Initiative Tracker

As a relatively newly minted D&D DM, there’s nothing I hate more than trying to track initiative (and more to the point) keep my players on track in combat. I dabbled with several different pre-built tools, but nothing really worked well for me. The main problem I kept running into is that trackers either took too long to setup, killing the momentum and terror players might feel about being suddenly thrust into combat, and/or also didn’t clearly indicate to the players who was up (and more importantly) who was on deck.
Inspiration struck when I found this amazing project: turing-smart-screen-python which allows writing your own information to many of those small USB screens typically meant to display PC stats like temp and fan speeds inside computer cases. I wondered if I could use one of these as a little initiative tracker that I could clip to the top of my DM screen. TLDR: Yes you can, and it is amazing!
If you just want to get up and running quickly, you can simply go purchase one of the screens supported by the above library, and then go read the instructions in my project repo: InitiativeTracker
Otherwise, stick around for a bit more details.
The majority of the code was relatively straightforward. First I built my configuration around a fairly simple JSON format. Initiative is a list of key/value pairs where the key is the player / entity name and the value is the initiative score. I later split the enemies into their own list so that I can ensure two things. The first is that the enemy will always appear lower in the list than the player of the same initiative score. Second, by splitting out enemy entities, I can color them with an alternative color scheme if I like.
Above and beyond color configuration settings, I’m especially pleased with my simple code format for removing players from the initiative and auto-rolling initiative scores in a pre-determined range. I like to setup all my expected fights in separate configuration files prior to a D&D session, then I can quickly grab player initiatives and have my monsters automatically and randomly roll their initiative on the fly, turning the whole process into an under a minute operation. Of course not all fights are pre-planned. When an unexpected fight occurs, I pre-prep by having the default initiative.json
file with all my player’s names already specified and a default enemy of "enemy": [0,20]
. I can still quickly populate the enemy list with a list of enemies, add a -
symbol in front of any players that aren’t in this initiative, and grab their scores to plug in quickly.
This project was the first project that I spent any real time 3D modeling. While the ultimate result of my modeling is very functional, it certainly isn’t complex, but I was very happy with the outcome. I can clip the screen to my DM screen where all my players can see the initiative order and who is up as well as “on-deck”. Meanwhile, I can look at the console on my laptop to read the same information. This provides everyone with the relevant details.
One thing I absolutely love about these USB screens is that they do not present as separate HDMI screens to the OS. This is really helpful for me because I do not have to screw around with trying to configure an app to display on my second screen, or worry about leaking sensitive notes to players while setting it up. The down side is that their refresh rate is not very quick, but this is a minor complaint and the whole screen updates in about 3 seconds.
While I greatly appreciate the turing-smart-screen project for all of the hard work of reverse engineering these various screen models, the process was not without it’s frustrations. A good deal of time was required to figure out how to make the screen format initiative details into a clean format. The project masks a lot of the ugly details which ultimately result in using in the ImagePIL library to create a bitmap of the text sent and displaying that on the screen. The side-effect of this is that you have no straightforward way of using the wrapper library to know how much space your font takes up. While I could have gone directly to the ImagePIL library, I was able to avoid this extra step by using a monospaced font and some trial and error to find sizes and guard rails that should work for my purposes.
The biggest pain points came from some anti-patterns that the underlying library used that I had to hack around. The first one was that the library was kind of secondary to the UX interface that is also developed by the same author. I kind of had to grab the folders in the project that I wanted and separate them out so as to grab only the code I needed. The library itself isn’t served in PyPi or released separately as a WHL.
The next issue was a minor annoyance of the library writing a log file on every run, with no way to disable this. I solved this problem by shadowing the logging library with my own that effectively does nothing. After that, I ran into another issue. The library does a frankly insane practice of trying to call sys.exit
catching any exceptions (which this will always cause as it will raise a SystemExit exception) and then call os._exit
. I cannot imagine what would possess someone to do this, but it makes catching any errors upstream a nightmare. This would exhibit itself whenever someone would run my program with no screen plugged-in or otherwise in a detectable state. The upstream library would simply hard-exit, causing my program to crash with no message. To work around this issue I ended up writing a context which monkey-patches out sys.exit
while in the process of setting up the library and connecting to the screen. You can see how that is implemented here: https://github.com/shellster/InitiativeTracker/blob/1828490fae022c0ab73ce156ffedc03f12bc7634/tracker.py#L37
With these issues addressed, I was able to produce a product that works and works very well for my use cases. I hope other TTRPG nerds also find it to be useful.
A note on licensing: I’m normally a huge proponent of releasing all my projects under MIT. Unfortunately the upstream library is GPL-3.0, which meant that I really didn’t have much choice in picking the license for this project unless I wanted to make the usage and modifications of the upstream library even more painful for other users. Since I wanted to strip all the extra “garbage” that I didn’t need out of the dependencies, and since there is no PYPI WHL I can just add as a dependency, I chose the path of least resistance and am releasing my code as GPL-3.0. If you would like to do something that requires MIT licensing, you have my blessing to strip out the upstream library and use my specific code as if it were MIT licensed.