Skip to content

Build a D&D 5e Gameplay Simulation, Part 8

  • by

Part 8: Radical Races

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 explore what role character race will play in the simulation.

In researching for this post I ran across the old basic rules and remembered how at one point what a player was (their race) and what they did (their class) were kind of smushed together into a single list of classes: Clerics, Dwarves, Elves, Fighters, Halflings, Magic Users, and Thieves.   When Advanced Dungeons & Dragons hit the scene, the idea of a character having a race AND a class was included.   Over the versions that followed, how they are implemented has changed a lot.  In 5e, things have been simplified a great deal, thankfully, making the work to do for a simulation a bit easier.  None the less, most folks would have to agree that the race a character selects will affect their performance.  One of the goals for the simulation would be to allow for the series of encounters that quantify  that impact to some degree.

Picking a race for a character gives them some ability traits like Darkvision (seeing well in low light situations) or Keen Senses (proficiency in the Perception skill).  It also gives the character Ability Score Increases that are added into the character’s ability array (ability arrays were discussed in part 5).  There have been some discussions around not using the racial ability array bonuses.  Being able to quantify the impact of that would be helpful. 

The first milestone in quantifying impact is to facilitate the generation of a Character automatically.  The first task in building a Character automatically is to build a working CharacterRace class.  

Requirements for the CharacterRace class:

  • Be able to return a randomly selected CharacterRace object.
  • The string passed as the CharacterRace name when the class is instantiated will be evaluated.
  • The object will contain all the pertinent information and only that information for CharacterRace.
  • The information for CharacterRace will be retrieved from the database.
  • Racial ability traits will be defined by name in a list of their own.  This list will be used later to populate the ability traits for a Character.
  • An optional argument for instantiating the class will define whether to use the racial ability score modifier bonuses (useRASMInd). The default value for this argument will be True.

Running this class depends on the database being loaded with the racial lookup data.  So before the tests for the class will work, make sure that the database is running and has current data.  To do that, docker-compose has a  “ps” command that will show things running currently.  Change directories into the docker directory from the repository and try that command.

(rpg) ~$ cd Git/python_rpg_sim/docker
(rpg) ~/Git/python_rpg_sim/docker$ docker-compose ps
Name   Command   State   Ports
------------------------------

(rpg) ~/Git/python_rpg_sim/docker$

On my system there wasn’t anything running.  To start the network and containers up with the docker-compose, use the “up -d” command, then try the ps command again.

(rpg) ~/Git/python_rpg_sim/docker$ docker-compose up -d
Creating network "docker_default" with the default driver
Creating pgs ... done
(rpg) ~/Git/python_rpg_sim/docker$ docker-compose ps
Name              Command              State           Ports         
---------------------------------------------------------------------
pgs    docker-entrypoint.sh postgres   Up      0.0.0.0:5433->5432/tcp

The ps command then showed that the service defined as pgs has a current state of “Up” and shows that the correct port has been mapped (5433).  With the database in place the tests for CharacterRace should now work.

(rpg) ~/Git/python_rpg_sim/docker$ cd ../Python
(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: ~/Git/python_rpg_sim/Python, inifile:
collected 36 items                                                             

test/test_CharacterRace.py F...................................          [100%]

=================================== FAILURES ===================================
______________________________ test_Race_Default _______________________________

    def test_Race_Default():
        db = InvokePSQL()
        a = CharacterRace(db)
        assert(len(a.race) > 3)
>       assert(a.ability_bonuses[0] != 0 or
               a.ability_bonuses[1] != 0 or
               a.ability_bonuses[3] != 0 or
               a.ability_bonuses[4] != 0 or
               a.ability_bonuses[5] != 0)
E       assert (0 != 0 or 0 != 0 or 0 != 0 or 0 != 0 or 0 != 0)

test/test_CharacterRace.py:13: AssertionError
===================== 1 failed, 35 passed in 0.32 seconds ======================

In my first pass through of the tests, the default case failed due to the assumption that ability bonuses are used, every race will have some kind of bonus.  Since this case was selecting a random race on definition, this wouldn’t always fail.  Fortunately, looking closer at the code showed that I had made an error in cycling through the list to see if  all the values were zero or not.  Turns out there was a MUCH more elegant way to handle those types of asserts.  More generally, any time that an iterable object needs to be checked for “all” or “any” type cases.  They are the built-in functions called “all()” and “any()”.   To fix the initial error, I changed the test code from what is above with the error, to:

def test_Race_Default():
    db = InvokePSQL()
    a = CharacterRace(db)
    assert(len(a.race) > 3)
    assert(any([v != 0 for v in a.ability_bonuses]))

This time the tests ran successfully. 

(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 36 items                                                             

test/test_CharacterRace.py ....................................          [100%]

========================== 36 passed in 0.25 seconds ===========================

Of course, this points out a weak point in my testing.  If I’m only testing for the ability bonuses in the random case, a mistake assigning the values might slip through.  All the races already have test cases that look to make sure the ability traits are correct, adding this assert wouldn’t be hard.  After making that change I tried the tests again and sure enough:

(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 36 items                                                             

test/test_CharacterRace.py .........F..........................          [100%]

=================================== FAILURES ===================================
______________________________ test_Race_Halfelf _______________________________

    def test_Race_Halfelf():
        db = InvokePSQL()
        a = CharacterRace(db, 'Half-Elf')
        assert(a.race == 'Half-Elf')
        darkvision_ind = 0
        fey_ancestry_ind = 0
        skill_versatility_ind = 0
        for b in a.traitContainer.traits:
            if (b.trait_name == 'Darkvision'):
                darkvision_ind = 1
            if (b.trait_name == 'Fey Ancestry'):
                fey_ancestry_ind = 1
            if (b.trait_name == 'Skill Versatility'):
                skill_versatility_ind = 1
        assert(darkvision_ind == 1)
        assert(fey_ancestry_ind == 1)
        assert(skill_versatility_ind == 1)
>       assert(any([v != 0 for v in a.ability_bonuses]))
E       assert False
E        +  where False = any([False, False, False, False, False, False])

test/test_CharacterRace.py:160: AssertionError
===================== 1 failed, 35 passed in 0.39 seconds ======================

The Half-Elf race was missing ability bonuses and failed its test.  I discussed how Liquibase manages the database objects in the last post.  The fix for this case was to update the file that had the info for the lu_racial_ability_score_modifier table to include the missing data, which for this discussion, was adding 2 to a Half-Elf’s charisma ability score.  That took inserting a change set entry in the file ( liquibase/changelog/dnd5e_lu_racial_ability_score_modifier.xml) 

<changeSet author="mdbdba" id="lu_racial_ability_score_modifier_2">
    <insert schemaName="dnd_5e" tableName="lu_racial_ability_score_modifier">
       <column name="race" value="Half-Elf"/> 
       <column name="ability" value="Charisma"/> 
       <column name="modifier" value="2"/> 
    </insert>
</changeSet>

The changeSet tag defines the new change set.  There is one action inside this change set and it’s an insert into the table.  The details for the action include the table name that’s being added to and each the column name with its value to insert.   

The Liquibase validate and migrate commands are then used to apply the changes.

(rpg) ~/Git/python_rpg_sim$ cd liquibase/changelog
(rpg) ~/Git/python_rpg_sim/liquibase/changelog$ liquibase validate
No validation errors found
Liquibase 'validate' Successful
(rpg) ~/Git/python_rpg_sim/liquibase/changelog$ liquibase migrate
Liquibase Update Successful

With that done, the tests ran successfully.

(rpg) ~/Git/python_rpg_sim/liquibase/changelog$ cd ../../Python
(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 36 items                                                             

test/test_CharacterRace.py ....................................          [100%]

========================== 36 passed in 0.25 seconds ===========================

That was quite a bit to cover in one post.  There is some info about how to work with docker-compose, how to change pytest asserts using “all()” and “any()” Python built-in functions, and a primer on how to make changes to the database using Liquibase.  For more information on the races themselves, look at the lu_race and lu_racial_* tables in the rpg database.  The Python/test/test_CharacterRace.py file has examples of how to create an instance of a CharacterRace class.  Next time, I will cover the relationships between the racial tables and how I add a race.   Until then, go play!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.