Part 9: Random Racial Traits
This series of posts is dedicated to building tools that support a simulation of simplified D&D gameplay. To implement the simulation I’ll be using git, Python, PostgreSQL, Elastic, and Docker. The idea behind these blog posts is to document my progress through this project and interesting bits I find along the way. If you’d like to start from the beginning, have a look at part 1, where I talked about where this idea came from, and what I’d like to accomplish. The github repository for this project holds the latest version of the files mentioned in these posts. To follow along, part 6 describes how to set up the environment. In this part, glad to get back to the D&D aspect of this project, I look at the random traits that add flavor to the characters.
When creating the characters to be used in the simulations, there are some racial traits that make them more interesting, but don’t figure into how the character would perform. They are eye color, hair color, hair type, skin tone, first name, alignment preference, and last name. Even if the answer is “None”, like Dragonborn hair color and type, it helps for us to visualize that character.
For all of these types of racial trait, I am using a random assignment pattern based on their sub-race or race depending on if there are any specific differences. The method for assigning alignment is different that the rest of the random racial traits. Alignment is based on a percentage roll, while the others are just random assignments.
To add a race into the simulation there quite a few tables to populate. They are “lu_race” and any table whose name starts with “lu_racial_”. They all describe values, bounds, or specific traits that are associated with the race values in the “lu_race” table. It will be easier to describe the process by actually adding a race. I will be adding the Dryad homebrew race.
I should add a disclaimer here. When I started playing again after decades, I was surprised by how great all the changes since second edition had made the game. Combat was easier to orchestrate and understand. The race and class combinations allow for anything I’ve dreamed up so far. And the rules are flexible enough that, with the right DM, games are no longer only bound to finding and killing things.
As much fun as fifth edition is, there is one thing that I get tripped up on when playing. Trying to find something about my character in a hurry is always a challenge. Distilling the words used to describe the mechanics of the game into workable data is exactly what I ran into when starting to work with races. The current data model comes from that angle, trying to impose order into almost total freedom. There’ll be some interpretation and I’ll try to explain those in these posts. Especially if it might be difficult to decipher from the code or data.
The lu_race table will be the place to start. In approaching this add, I’ll be making additions in the liquibase/changelog/myContent directory. That way changes to the files in changelog parent directory will not affect anything someone following along might do. Create the liquibase/changelog/myContent/lu_race.xml file with the following.
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd"> <changeSet author="mdbdba" id="my_lu_race_1"> <insert schemaName="dnd_5e" tableName="lu_race"> <column name="race" value="Dryad"/> <column name="maturity_age" value="5"/> <column name="avg_max_age" value="150"/> <column name="base_walking_speed" value="30"/> <column name="height_min_inches" value="46"/> <column name="height_modifier_multiplier" value="2"/> <column name="height_modifier_die" value="12"/> <column name="height_modifier_adj" value="0"/> <column name="weight_min_pounds" value="95"/> <column name="weight_modifier_multiplier" value="5"/> <column name="weight_modifier_die" value="10"/> <column name="weight_modifier_adj" value="0"/> <column name="size" value="Medium"/> <column name="source_material" value="homebrew"/> <column name="source_credit_url" value="https://www.dndbeyond.com/characters/races/2716-dryad"/> </insert> </changeSet> </databaseChangeLog>
For Liquibase, the label for the change set needs to be unique, so in this case “my_lu_race_1”. The insert statement is fairly straight forward. The “column name” entries match the table field names. Most of the values for the fields I got from the class definition. The definition for the height and weight is broken down to a minimum, a multiplier (how many times to roll the die), and a die (how many sides). The class definition included the height, but not the weight, since there are simularities between Dryads and elves, I used the weight calculation from the Elf class.
Racial ability score increases are kept in the lu_racial_ability_score_modifier table. There is a YAML file in liquibase/changelog that matches that name for reference, I will create a new one as liquibase/changelog/myContent/lu_racial_ability_score_modifier.yml
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd"> <changeSet author="mdbdba" id="mu_lu_racial_ability_score_modifier_1"> <insert schemaName="dnd_5e" tableName="lu_racial_ability_score_modifier"> <column name="race" value="Dryad"/> <column name="ability" value="Wisdom"/> <column name="modifier" value="1"/> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_ability_score_modifier"> <column name="race" value="Dryad"/> <column name="ability" value="Charisma"/> <column name="modifier" value="2"/> </insert> </changeSet> </databaseChangeLog>
In this file I’m placing two records into the table, both for the Dryad class. One gives a +1 bonus to Wisdom, and the other gives a +2 bonus to Charisma.
The definition of alignment is a bit trickier. Generally, alignment preferences are a bit vague, so take a bit of liberty with it. The idea is to take the most likely alignments the race might pick and assign a percentage to it. The description for the alignment from Dryad’s page says:
"Dryads are most often neutral, with their main focus on tending to nature around them. Dryads working in union with other forest creatures in some form of alliance may tend toward lawfulness. Those deeply isolated in wild, untamed lands may be chaotic."
The assignment for the percentages goes into the lu_racial_alignment_preference table. One percentage for each race alignment combination.
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd"> <changeSet author="mdbdba" id="my_lu_racial_alignment_preference_1"> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="LG" /> <column name="pct_likely" value="1.0" /> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="NG" /> <column name="pct_likely" value="2.0" /> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="CG" /> <column name="pct_likely" value="5.0" /> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="LN" /> <column name="pct_likely" value="5.0" /> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="N" /> <column name="pct_likely" value="45.0" /> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="CN" /> <column name="pct_likely" value="34.0" /> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="LE" /> <column name="pct_likely" value="1.0" /> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="NE" /> <column name="pct_likely" value="2.0" /> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_alignment_preference"> <column name="race" value="Dryad" /> <column name="alignment" value="CE" /> <column name="pct_likely" value="5.0" /> </insert> </changeSet> </databaseChangeLog>
I’ve given the likelihood of the alignments to be:
- Lawful Good 1%
- Neutral Good 2%
- Chaotic Good 5%
- Lawful Neutral 5%
- Neutral 45%
- Chaotic Neutral 34%
- Lawful Evil 1%
- Neutral Evil 2%
- Chaotic Evil 5%
Dryad names closely resemble Elf female first names and they don’t really have a last name unless it’s the forest they come from.
First name records populate the liquibase/changelog/myContent/lu_racial_first_name.yml file. Each entry in the change set for that table contains an insert like the following. Abbreviated because that file would be long to list here.
... <insert schemaName="dnd_5e" tableName="lu_racial_first_name"> <column name="race" value="Dryad"/> <column name="value" value="Ethissia"/> <column name="gender" value="U"/> </insert> ...
All of the names are listed under the Universal gender value because the article didn’t actually say all Dryads were female, just had some characteristics of Elf women. That way, regardless of gender for the race, the same name list will get used.
Since the Dryad don’t usually have last names, I will put a record in the lu_racial_last_name table that identifies that to the simulation.
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd"> <changeSet author="mdbdba" id="my_lu_racial_last_name_1"> <insert schemaName="dnd_5e" tableName="lu_racial_last_name"> <column name="race" value="Dryad"/> <column name="value" value="None"/> <column name="gender" value="U"/> </insert> </changeSet> </databaseChangeLog>
The files for skin tone (color of sunlight, moonlight), hair color (fall or spring leaves, rose petals), hair type (woven), eye color (black, green, red, yellow), and languages (Common, Elvish, and Sylvan) are made the same way, their respective files found under liquibase/changelog/myContent.
The abilities associated with each race are a bit more complicated to quantify in data. The design I’m using is to pair the name of the trait along with a category (lu_racial_trait_category) and try to roughly communicate things that might differ between different races (like how often a trait can be used or if it applies to a specific ability) in the lu_racial_trait table.
Even when doing that, some explanation and extra code may be required to implement the traits.
<insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Darkvision"/> <column name="category" value="Vision"/> </insert>
The first trait is easy enough. It affects vision, so that is the racial trait category. Dryads can see in very low light. This is a trait shared by Dwarves and Elves. It will come into play when we define different types of environments used in the simulation later.
Speak with Beasts and Plants is a bit more complicated.
<insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Speak with Beasts and Plants 1"/> <column name="category" value="Spell"/> <column name="affected_name" value="Speak with Plants"/> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Speak with Beasts and Plants 2"/> <column name="category" value="Spell"/> <column name="affected_name" value="Speak with Animals"/> </insert>
This trait can be broken into two separate spells, speak with Plants, and speak with Animals. So, the trait is broken into two entries under the “Spell” racial trait category. Effectively, when a Dryad uses this trait, it’s going to be the same as the spell for our purposes.
Fey Blessing adds needing to be recharged.
<insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Fey Blessing 1"/> <column name="category" value="Saving Throw"/> <column name="affected_name" value="Intelligence"/> <column name="affect" value="advantage"/> <column name="recharge_on" value="Long or Short Rest"/> <column name="description" value="targetAffect:Magic"/> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Fey Blessing 2"/> <column name="category" value="Saving Throw"/> <column name="affected_name" value="Wisdom"/> <column name="affect" value="advantage"/> <column name="recharge_on" value="Long or Short Rest"/> <column name="description" value="targetAffect:Magic"/> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Fey Blessing 3"/> <column name="category" value="Saving Throw"/> <column name="affected_name" value="Charisma"/> <column name="affect" value="advantage"/> <column name="recharge_on" value="Long or Short Rest"/> <column name="description" value="targetAffect:Magic"/> </insert>
This trait effects a Saving Throw once per long or short rest. “Saving Throw” is the racial trait category. Because it can be used for either an Intelligence, Wisdom, or Charisma Saving throw, the trait is broken into three rows, each having the ability as the “affected_name” and a recharge_on value set to “Long or Short Rest”. The code will interpret this as a single use before having to recharge.
Woodland Magic adds a level restriction to the definition.
<insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Woodland Magic 1"/> <column name="category" value="Spell"/> <column name="affected_name" value="Druidcraft"/> <column name="recharge_on" value="Long Rest"/> <column name="description" value="onLevel:1"/> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Woodland Magic 2"/> <column name="category" value="Spell"/> <column name="affected_name" value="Charm Person"/> <column name="recharge_on" value="Long Rest"/> <column name="description" value="onLevel:3"/> </insert> <insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Woodland Magic 3"/> <column name="category" value="Spell"/> <column name="affected_name" value="Barkskin"/> <column name="recharge_on" value="Long Rest"/> <column name="description" value="onLevel:5"/> </insert>
Like “Speak with Beasts and Plants” this is another trait allowing for Spells. This one has a recharge of a Long Rest. The description field is used to add the level restrictions using the “onLevel” tag.
Forest Step makes use of the description field in a different way.
<insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Forest Step"/> <column name="category" value="Spell"/> <column name="affected_name" value="Tree Stride"/> <column name="description" value="Cost:Movement(15feet);Range:60feet"/> </insert>
In this case, the “Cost” tag is used to describe that the cost of using this trait for our purposes is 15 feet of the characters movement and has to be done in a range of 60 feet.
Treebound Armor affects the character’s Armor Class by adding 3 to the usual base of 10.
<insert schemaName="dnd_5e" tableName="lu_racial_trait"> <column name="race" value="Dryad"/> <column name="name" value="Treebound Armor"/> <column name="category" value="Armor Class"/> <column name="affected_name" value="base armor class"/> <column name="affected_adj" value="3"/> </insert>
With all that being done, I created a test for the class, adding it to Python/test/test_CharacterRace.py
def test_Race_Dryad(): db = InvokePSQL() a = CharacterRace(db, 'Dryad') assert(a.race == 'Dryad') darkvision_ind = 0 speak_with_beasts_and_plants_ind = 0 fey_blessing_ind = 0 woodland_magic_ind = 0 forest_step_ind = 0 treebound_armor_ind = 0 for b in a.traitContainer.traits: if (b.trait_name == 'Darkvision'): darkvision_ind = 1 if (b.trait_name == 'Speak with Beasts and Plants'): speak_with_beasts_and_plants_ind = 1 if (b.trait_name == 'Fey Blessing'): fey_blessing_ind = 1 if (b.trait_name == 'Woodland Magic'): woodland_magic_ind = 1 if (b.trait_name == 'Forest Step'): forest_step_ind = 1 if (b.trait_name == 'Treebound Armor'): treebound_armor_ind = 1 assert(darkvision_ind == 1) assert(speak_with_beasts_and_plants_ind == 1) assert(fey_blessing_ind == 1) assert(woodland_magic_ind == 1) assert(forest_step_ind == 1) assert(treebound_armor_ind == 1) assert(any([v != 0 for v in a.ability_bonuses])) alignmentObj = a.getAlignment(db) assert('abbreviation' in alignmentObj) assert('alignment' in alignmentObj) assert(a.getSkinTone(db)) assert(a.getHairColor(db)) assert(a.getHairType(db)) assert(a.getEyeColor(db)) assert(a.getName(db, 'U'))
The test just verifies all the things we’ve added actually work.
(rpg) ~/Git/python_rpg_sim/Python$ pytest test/test_CharacterRace.py ============================= test session starts ============================== platform linux -- Python 3.6.5, pytest-3.8.2, py-1.6.0, pluggy-0.7.1 rootdir: /home/mdbdba/Git/python_rpg_sim/Python, inifile: collected 38 items test/test_CharacterRace.py ...................................... [100%] ========================== 38 passed in 0.52 seconds ===========================
That’s a lot about races and their traits. Next time, I’ll start looking at character class and how I’m going to be building player characters in the simulation. Until then, Go Play!