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
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.