Skip to content

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

  • by

Part 12: Creating Complex Characters.

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. It talks 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.  Up to this point all the pieces have been put in place to create a character. In this part, it’s time to look at what can be made with these pieces.

Wow, it seems like I just posted part 11 yesterday, put my head down on how to make characters happen and months slipped by! Better get to it! Okay, one of the first realizations folks have when starting to play D&D is that character creation is a pretty complex. It should be, for them it’s going to be the portal through which the games comes alive. There is no quick way to do that. An experienced Dungeon Master (DM) will have some tricks and ideas to help players get their character built more quickly, but the great part of the game is that this part so flexible. In building the gameplay simulation, the same ideas apply. All of the combinations and depth of detail possible make character creation tricky. For the purpose of this series of blogs, the focus is on what’s available in the System Reference Document (SRD), so the complexity is limited a bit, but should make for a decent challenge.

Character lineage

“I think the Dwarf likes your boots”

What do a human fighter, a skeleton warrior, and a Thri-kreen shop clerk all have in common?

In D&D lore, the characters and their races have many different beginnings. Tieflings are descendants of a cursed empire, Half-orcs are the product of mixed parentage, while others are said to be descendant of the gods strong enough to pass their likenesses on to the multiverse. How a DM works those origin stories in during a campaign depends on the age and maturity of the folks playing, but in this simulation the origin of every type of character is traced back to a single spot. That single spot is a Python class, and it is called Character.py.

In planning out how encounters between heroes and their opponents would work, it became apparent that the most efficient way to build the simulation would be to have the Objects taking place in an encounter have as much overlapping functionality (at least in name) as possible. That way if the goal would be to simulate a fist fight between party members, the party attacking a Green dragon, or a scouting party of Orcs attacking a hero, all of the scenarios would boil down to the same types of interactions and calls. Yes, even if the hero being attacked is a bard specializing in the seducing of Orcs. Where else do half-orcs come from?

Thinking how to describe this in terms that cover both D&D and python, a class called Character is going to be used by subclasses PlayerCharacter, Foe, and NonPlayerCharacter to provide all of the common functionality they require.

Character()
PlayerCharacter(Character)Foe(Character)NonPlayerCharacter(Character)

For the simulation, the focus will be on the PlayerCharacter and Foe classes. Although it would be helpful to be able to generate a shopkeep or captian of the guard using the NonPlayerCharacter class, that will be the subject of later posts as it isn’t required for the simulation.

Character

The Character class holds the bare minimum definitions for all three subclassses. They all:

  • have abilities
  • have an armor class
  • deal and take damage
  • make ability checks
  • make saving throws
  • can be healed
  • can die and be revived
  • can be blinded
  • can be charmed
  • can be deafened
  • can be fatigued
  • can be frightened
  • can be grappled
  • … etc

The Character class defines these attributes so that the subclasses don’t have to unless there is a reason to override the default. There are currently two ways to look into how the characters are interacting. Each covers different types of information. The first is by way of debug logging. That will dump a Player Character or Foe definition to standard out as well as information on any action they take. The other way shares information about the data generated for the character, it is a list of arrays called the ClassEval list.

In addition to the random values generated for the player character, like hair color, eye color, or racial name, there is a guide for roleplaying being generated with the Character class. In this code it is called the Taliesin Temperament Architype (TTA). It comes from an episode of GM tips with Satine Pheonix where Taliesin Jaffe shared some of his ideas about role playing a character. This method combines where to produce the voice (low, medium, or high) with an attitude based on the seven dwarfs (Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, and Doc). TTA is an inherited attribute from Character and appears in the example output from the PlayerCharacter and Foe classes below.

Player Character

The PlayerCharacter class is the pulling together of everything that’s been worked on so far. A set of ability stats, a race, a class, traits related to that race class combination, and other randomly selected traits are all parts of the PlayerCharacter. The creation of a PlayerCharacter is stored in the character table in the database. That way it could be reused later. Generating a random PlayerCharacter looks like this:

#!/usr/bin/env python
# gen_pc.py
from InvokePSQL import InvokePSQL
from PlayerCharacter import PlayerCharacter


db = InvokePSQL()
print("Debug info follows")
a1 = PlayerCharacter(db=db, debugInd=1)
print("ClassEval info follows")
for i in range(len(a1.getClassEval())):
    for key, value in a1.getClassEval()[i].items():
        print(f"{i} -- {str(key).ljust(25)}: {value}")
$ chmod +x gen_pc.py
$ ./gen_pc.py
Debug info follows
2019-03-30 18:39:48,491 - DEBUG - Zusig The Wretched: armorClass set to 10
2019-03-30 18:39:48,492 - DEBUG - Zusig The Wretched: reset movement to 30
2019-03-30 18:39:48,493 - DEBUG - Zusig The Wretched: armorClass set to 10
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: PlayerCharacter
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Name:         Zusig The Wretched
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Id            2
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: TTA:          Happy/Low
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Gender:       U
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Race:         Half-Orc (SRD5)
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Movement:     30
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Class:        Barbarian (SRD5)
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Armor Class:  10
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Level:        1
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Hit Die:      12
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Hit Points:   14 / 14
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Prof Bonus:   2
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Height:       6'5"
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Weight:       197 pounds
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Alignment:    Chaotic evil
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: AlignAbbrev:  CE
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Skin Tone:    White
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Hair Color:   Black
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Hair Type:    Wavy
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Eye Color:    Grey
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Size:         Small
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Finesse Ability: Strength
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Raw Ability Array:  [10, 14, 15, 11, 11, 11]
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Ordered Array:      [15, 14, 11, 11, 11, 10]
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Sort Array:    ['Strength', 'Constitution', 'Dexterity', 'Wisdom', 'Charisma', 'Intelligence']
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Nbr Sort Array:     [0, 2, 1, 4, 5, 3]
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: Sorted:             [15, 11, 14, 10, 11, 11]
2019-03-30 18:39:48,496 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Ability         Mod  Total = (Base + Racial + Level Improvements)
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Strength          3   17   = (15  +    2   +    0)
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Dexterity         0   11   = (11  +    0   +    0)
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Constitution      2   15   = (14  +    1   +    0)
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Intelligence      0   10   = (10  +    0   +    0)
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Wisdom            0   11   = (11  +    0   +    0)
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Charisma          0   11   = (11  +    0   +    0)
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Languages:
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched:    Common
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched:    Orc
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: Proficiencies:
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched:    Intimidation            (Menacing)
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: trait_name:                   Darkvision
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: trait_orderby:                1
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: category:                     Vision
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: trait_name:                   Relentless
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: trait_orderby:                1
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: category:                     Damage Received
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: affected_name:                Hit Points
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: affected_adj:                 1
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: recharge_on:                  Long Rest
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: description:                  Instead of dropping below 0HP, drop to 1
2019-03-30 18:39:48,497 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched: trait_name:                   Savage Attacks
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched: trait_orderby:                1
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched: category:                     Damage Given
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched: affected_name:                Melee Attack
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched: affect:                       additional damage die
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched: description:                  targetAffect:Crit
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched: 
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched: Class Features:
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched:  Barbarian 1 proficiency_bonus Proficiency Bonus Value 2
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched:  Barbarian 1 Rage Rages Per Long Rest 2 Rage Damage 2
2019-03-30 18:39:48,498 - DEBUG - Zusig The Wretched:  Barbarian 1 Unarmored defense

ClassEval info follows
0 -- pythonClass              : Character
0 -- genderCandidate          : Random
0 -- abilityArrayStr          : Common
0 -- level                    : 1
0 -- debugInd                 : 1
0 -- TTA                      : Happy/Low
0 -- Gender                   : U
1 -- pythonClass              : PlayerCharacter
1 -- newCharacter             : True
1 -- raceCandidate            : Random
1 -- classCandidate           : Random
1 -- genderCandidate          : Random
1 -- abilityArrayStr          : Common
1 -- level                    : 1
1 -- debugInd                 : 1
1 -- RaceToUse                : Half-Orc
1 -- hitPoints_level_1        : 14
1 -- armorClass               : 10
1 -- Proficiency Bonus        : 2
1 -- Damage Adjust            : Acid: , Bludgeoning: , Cold: , Fire: , Force: , Ligtning: , Necrotic: , Piercing: , Poison: , Psychic: , Radiant: , Slashing: , Thunder: 
1 -- characterId              : 2
1 -- race                     : Half-Orc
1 -- class                    : Barbarian
1 -- gender                   : U
1 -- rawAbilityArray          : 10,14,15,11,11,11
1 -- sortedAbilityArray       : 15,11,14,10,11,11
1 -- racialAbilityArray       : 2,0,1,0,0,0
1 -- abilityImpArray          : 0,0,0,0,0,0
1 -- abilityArray             : 17,11,15,10,11,11

Foe

The Foes that the simulation will be using will have less randomness associated with them. A majority of of the data for them is stored in the foe table. Generating a random Foe looks like this:

#!/usr/bin/env python
# gen_foe.py
from InvokePSQL import InvokePSQL
from Foe import Foe

db = InvokePSQL()
print("Debug info follows")
a1 = Foe(db, debugInd=1)
print("ClassEval info follows")
for i in range(len(a1.getClassEval())):
    for key, value in a1.getClassEval()[i].items():
        print(f"{i} -- {str(key).ljust(25)}: {value}")
$ chmod +x gen_foe.py
$ ./gen_foe.py
Debug info follows
2019-03-30 18:57:53,167 - DEBUG - Skeleton: armorClass set to 13
2019-03-30 18:57:53,168 - DEBUG - Skeleton: 
2019-03-30 18:57:53,168 - DEBUG - Skeleton: Foe
2019-03-30 18:57:53,168 - DEBUG - Skeleton: gender: U
2019-03-30 18:57:53,168 - DEBUG - Skeleton: name: Skeleton
2019-03-30 18:57:53,168 - DEBUG - Skeleton: foe_type: Undead
2019-03-30 18:57:53,168 - DEBUG - Skeleton: size: Medium
2019-03-30 18:57:53,168 - DEBUG - Skeleton: alignment: Lawful evil
2019-03-30 18:57:53,168 - DEBUG - Skeleton: alignment abbrev: LE
2019-03-30 18:57:53,168 - DEBUG - Skeleton: base_walking_speed: 30
2019-03-30 18:57:53,168 - DEBUG - Skeleton: challenge_level: 0.25
2019-03-30 18:57:53,168 - DEBUG - Skeleton: ability_array_str: 10,14,15,6,8,5
2019-03-30 18:57:53,168 - DEBUG - Skeleton: abilityArrayStr: [10, 14, 15, 6, 8, 5]
2019-03-30 18:57:53,168 - DEBUG - Skeleton: ability_modifier_array: [0, 2, 2, -2, -1, -3]
2019-03-30 18:57:53,168 - DEBUG - Skeleton: hit_point_die: 8
2019-03-30 18:57:53,168 - DEBUG - Skeleton: hit_point_modifier: 2
2019-03-30 18:57:53,168 - DEBUG - Skeleton: hit_point_adjustment: 4
2019-03-30 18:57:53,168 - DEBUG - Skeleton: standard_hit_points: 13
2019-03-30 18:57:53,168 - DEBUG - Skeleton: armor_class: 13
2019-03-30 18:57:53,168 - DEBUG - Skeleton: hit_points: 20
2019-03-30 18:57:53,168 - DEBUG - Skeleton: cur_hit_points: 20
2019-03-30 18:57:53,168 - DEBUG - Skeleton: temp_hit_points: 0
2019-03-30 18:57:53,168 - DEBUG - Skeleton: ranged_weapon: Shortbow
2019-03-30 18:57:53,168 - DEBUG - Skeleton: melee_weapon: Shortsword
2019-03-30 18:57:53,168 - DEBUG - Skeleton: ranged_ammunition_type: Arrow
2019-03-30 18:57:53,168 - DEBUG - Skeleton: ranged_ammunition_amt: 20
2019-03-30 18:57:53,168 - DEBUG - Skeleton: armor: Scraps
2019-03-30 18:57:53,168 - DEBUG - Skeleton: shield: None
2019-03-30 18:57:53,169 - DEBUG - Skeleton: source_material: SRD5
2019-03-30 18:57:53,169 - DEBUG - Skeleton: source_credit_url: None
2019-03-30 18:57:53,169 - DEBUG - Skeleton: source_credit_comment: None
2019-03-30 18:57:53,169 - DEBUG - Skeleton: 
ClassEval info follows
0 -- pythonClass              : Character
0 -- genderCandidate          : U
0 -- abilityArrayStr          : Common
0 -- level                    : 1
0 -- debugInd                 : 1
0 -- TTA                      : Grumpy/Low
0 -- armorClass               : 13
0 -- hitPoints                : 20
1 -- pythonClass              : Foe
1 -- foeCandidate             : Skeleton
1 -- challengeLevel           : .25
1 -- damageGenerator          : Random
1 -- hitpointGenerator        : Max
1 -- level                    : 1
1 -- debugInd                 : 1

And just like that there’s a universe of willing combatants for us to pit against each other. Now they need a place to face off. The next post will focus on building a virtual battlefield will get built and putting the combatants in place.

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.