The best way to create a Pathfinder character sheet? Python!

Python, Pathfinder

2019-08-25

I love playing Pathfinder. And I've tried many different ways to manage my character sheets. You can do it by hand, use an Excel-sheet or use one of the many available programs to do it for you. The problem with the programs is that even if they actually contain everything, you get into trouble if you want to use custom items or house rules. This is obviously no problem if you do it by hand but that is honestly kinda complex and it is very hard to make no mistakes. Lately, I've been writing one in markdown, and thought to myself: "If you could combine the possibilities from markdown and Excel, that would be very nice."

And I already had my Datasheet library which would basically provide me with the means to convert python code to html. So I upgraded it a little and started writing my character sheet with it. I wrote quite some functions that would be useful independent of my own character sheet and in the end decided to make a library out of it. Here, I will showcase this library but before we get to that: this is the result, and the source is here.

The library itself is up on github, but not on PyPi. To install it you will have to clone it. This library is far from complete as that would have been a ton of work. Therefore you will probably want an editable installation. If you make some useful changes please be so kind to send a pull-request. To be able to send a pull request you have to fork pypf on github first.

Since pip does not support editable installations of of a pyproject.toml file, we will use flit for that. Additionally we will need Datasheet too, so the complete setup process could look like this:

git$ pip install flit datasheet
git$ git clone <URL of your fork>
git$ cd pypf
pypf$ flit install -s

Start your python character sheet by importing the required libraries:

import pypf as pf
import datasheet as ds

The first thing is to write down the basic stats:

name = 'Levi'
sorc_lvl = 5
dd_lvl = 1
lvl = sorc_lvl + dd_lvl
caster_lvl = lvl - 1
abilities = pf.ability_table(14, 0, 
                             12, 0, 
                             15, 0, 
                             10, 0, 
                             12, 0, 
                             (16 + 2),  2) # first 2 by human, 2 by headband
concentration = caster_lvl + abilities.Cha.Mod
hp = (6 + 6 + 5 + 4 + 1 + 12 # Rolls
      + abilities.Con.Mod * lvl
      + lvl ) # Toughness bonus
bab = 2 + 0
cmb = bab + abilities.Str.Mod + 0 #special size mod

# A creature can also add any circumstance, deflection, dodge, insight, luck,
# morale, profane, and sacred bonuses to AC to its CMD. Any penalties to a
# creature’s AC also apply to its CMD. A flat-footed creature does Falset add its
# Dexterity bonus to its CMD.
cmd = (bab + 10 + abilities.Dex.Mod
       + 1 # Insect-helmet
       + 1) # Ring of deflection
fort = (1 # Sorc lvl 5
        + 1 # DD lvl 1
        + abilities.Con.Mod)
reflex = (1
          + 0 
          + abilities.Dex.Mod)
will = (4
        + 1
        + abilities.Wis.Mod)

This is very straight forward, just gather all your information, and you can easily add comments for you to remember where stuff comes from. Also you don't need to click one million times to enter formulas or change some settings of Excel. Don't you just love simple text files?

So far the only library specific thing is the ability_table() function. It will take the base and bonus value for each ability and return an AttrDict that represents this table with the columns base, bonus, final and modifier for every ability.

The next thing is to add the spells per day table:

def spdb(lvl): return pf.spells_per_day_bonus(abilities.Cha.Mod, lvl)
spells_per_day = {
    1: 6 + spdb(1),
    2: 4 + spdb(2),
    "Claws": 3 + abilities.Cha.Mod
}

I first define a shorthand function spdb() that calls the spells_per_day_bonus() function from the library with the corresponding modifier already set. I wrote that table down up to a modifier of +10, so if you should need a higher modifier, please adjust the table in pypf/pypf/abilities.py and submit a pull request.

Then I add some more stats, nothing library specific:

nat_attacks = {
    "2 x Claws (Magic dmg)": {
        "Atk": bab + 1 + abilities.Str.Mod, # Weapon focus Nat. weapons bonus
        "Dmg": f"1d4 + {abilities.Str.Mod}"} # Will increase to 1d6 on bloodline lvl 7
}


nat_armor = (1 # Dragon resistances
                + 1) # DD lvl 1
stacking_ac = 1 #insect helmet
deflection = 1 # ring of deflection
AC = {
    "Abs": 10 + nat_armor + stacking_ac + deflection + abilities.Dex.Mod,
    "Touch": 10 + deflection + abilities.Dex.Mod,
    "Flat": 10 + deflection + stacking_ac + nat_armor}

Next come the skills which can be really annoying: checking every possible skill, checking the required ability, checking whether it can be used untrained, noting whether it is a class skill so you can add the +3 bonus once you put your first rank in at a later point in time ...

This is all handled automatically as you can see here:

skills = pf.make_skill_table({
    'Bluff': 1,
    'Fly': 3, 
    'Intimidate': 1,
    'Knowledge (arcana)': 5,
    'Perception': 6,
    'Spellcraft': 3,
    'Use magic device': 1,
    'Linguistics': 1,
    'Knowledge (planes)': 1
}, abilities, 'Sorcerer', 'Draconic', 'Dragon Disciple')

All you have to do is to enter the skills you have actually put points in and tell the functions which classes apply. As it is important to spell them exactly how they are spelled in pypf/pypf/skills.py I made sure to only capitalize the first word, other than that it is carefully copied from the official table but if your result looks fishy you better check against the list that is stored in skills.py. Here you will (probably) have to edit the library again (remember the pull request) because I only entered the class skills for the classes I needed. They are defined in skills.py like this:

class_skill_masks = {
    "Sorcerer": make_class_skill_mask(
        'Appraise', 'Bluff', 'Craft', 'Fly', 'Intimidate', 'Knowledge (arcana)',
        'Profession', 'Spellcraft', 'Use magic device'),
    "Draconic": make_class_skill_mask('Perception'),
    "Dragon Disciple": make_class_skill_mask(
        'Diplomacy', 'Escape artist', 'Fly', 'Knowledge.*', 'Perception', 
        'Spellcraft')
}

All you have to do here is to add another entry for your class. Now, there are only 3 things missing that I want on my character sheet: the spells, the feats and the items. Those are just ordinary dictionaries, and I will only show an example here instead of the complete thing which you can find in the script.

spells_by_level = [
    {'Dancing Lights':
        f'V,S; 1min 1-4 lights that can freely move. Range: {100 + 10 * caster_lvl} ft',
     ...},
    ...]

Thanks to python, you can actually put in the rules for the scaling and don't need to adjust anything if you update your stats and it's way more readable than the Excel formulas.

Now, all required information is in the script so let's take a look at how we can make this into a nice looking html file which is were Datasheet comes into play. Datasheet renders a nice looking html file with a table of contents (TOC) from strings that are interpreted as markdown and some other types of data, that don't matter here. So, for starters: to just display a title and the ability table you would need the following code:

sheet = ds.Sheet('Levi', standalone=True)
sheet << '# Levi'
sheet << pf.md.table(abilities, "Ability"),
sheet.render()

The standalone means that only a single html file should be generated as output and nothing else. All single line strings that contain a heading will automatically end up in the TOC. All functions in the pf.md module will create markdown or html code (which is valid markdown too). There are three relevant functions:

  • table() for normal tables,
  • flat_table() for tables with a single row
  • and long_table() for tables with a single column and an index.

table() expects a table represented as a nested dict like this:

example_table = {
    'row1': {'col1': 'val11', 'col2': 'val12'},
    'row2': {'col1': 'val21', 'col2': 'val22'}
}

the other two table functions expect either a simple dict like this:

AC = {
    "Abs": 10 + nat_armor + stacking_ac + deflection + abilities.Dex.Mod,
    "Touch": 10 + deflection + abilities.Dex.Mod,
    "Flat": 10 + deflection + stacking_ac + nat_armor}

or a string containing all the keys separated by a comma, and the values as *args like this:

pf.md.flat_table('Abs, Sorc, DD, Caster',
                 lvl, sorc_lvl, dd_lvl, caster_lvl,
                 caption='Level')

As you can see, you can additionally provide a caption. Also you can provide alignments for the columns as a list that has one entry ("left", "right" or "center") for each column like this:

pf.md.table(nat_attacks, 'Weapon', caption='Natural attacks',
            col_align=['right', 'center', 'center'])

We could simply add all the tables and information we want to the sheet like this:

sheet = ds.Sheet('Levi', standalone=True)
sheet << '# Levi'
sheet << pf.md.table(abilities, "Ability"),
sheet << pf.md.flat_table('Abs, Sorc, DD, Caster',
                 lvl, sorc_lvl, dd_lvl, caster_lvl,
                 caption='Level')
sheet << pf.md.table(nat_attacks, 'Weapon', caption='Natural attacks',
            col_align=['right', 'center', 'center'])
...
sheet.render()

But that would not be a very nice usage of space. Therefore Datasheet provides two layouts, that can be used to align their children in a row or a column and can be nested arbitrarily. In my case that led to the complete code to generate my character sheet:

sheet = ds.Sheet('Levi', standalone=True)
sheet << '# Levi'
sheet << ds.HLayout((
            pf.md.long_table(skills, 'Name', caption="Skills", 
                             col_align=['right', 'center']), 
            ds.VLayout((
                pf.md.flat_table('Abs, Sorc, DD, Caster',
                                  lvl, sorc_lvl, dd_lvl, caster_lvl,
                                  caption='Level'),
                pf.md.table(abilities, "Ability"),
                pf.md.flat_table('Concent., HP, CMB, CMD, Fort, Ref, Will',
                                 concentration, hp, cmb, cmd, fort, reflex,
                                 will),
                ds.HLayout((
                    pf.md.flat_table(spells_per_day, 
                                     caption='Spells per day'),
                    pf.md.flat_table(AC, caption='AC'))),
                pf.md.table(nat_attacks, 'Weapon', caption='Natural attacks',
                            col_align=['right', 'center', 'center']),
                f'''
                    **Languages:** common, draconic  
                    Spell resistance DC for enemies: {10 + abilities.Cha.Mod} +
                    Spell-lvl (+ 1 for Evos) '''))))
sheet << '## Spells'
for i, spells in enumerate(spells_by_level):
    sheet << pf.md.long_table(spells, r_col_name='Effect', caption=f'Lvl {i}')
sheet << '## Feats'
sheet << pf.md.long_table(feats)
sheet << '## Items'
sheet << pf.md.long_table(items)
sheet.render()

All you need to do to get your html-character sheet is execute the script. And that's it. Have fun recreating your character sheet, and please make a pull request with your updates.