Skip to content

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

  • by

Part 16: Melee!

Building on the game of tag that was built in part 14, it’s time to get to some melee! Doing that will add a bunch of functionality that the other types of interaction: ranged, spells, diplomacy, and seduction, will share.

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.

Getting into Melee

Post 14 left off with a game objective where the winning team has the first player to tag one from another team. A “waiting” action was added so that a player could anticipate an incoming attack. So getting into melee range was pretty much the endgame. Now the objective is changing to be attrition, where only one party remains and melee can go on for several rounds between players. When that happens, there’s the opportunity for multiple players from a team being in melee with a single player from another team. That changes the dynamic for melee to be a one to many relationship between parties at any given time. To accommodate this, each player gets a list of players they are in melee with. That list grows and shrinks depending on who is within range and whether they are still standing.

Deciding which to attack is simplified to the closest character on the map.

Making an Attack

Each class has a default melee weapon that they will use for attacks. When they attack, the specifics for that weapon are used along with the character’s proficiency and skill bonuses. This happens in the Attack class of the character making the attack. The main two outputs of an attack are the attack value (Does a 37 hit?) and a potential damage.

The attack value is the sum of the natural d20 roll and the “to hit” bonus of the character (proficiency + any strength or other “to hit” bonuses).

If an attack is determined to be a critical hit, the rolled damage is doubled before adding any bonuses. The rolled damage, any critical bonus, and other bonuses are added to determine the potential damage of the attack.

Defending the Attack

The player being attacked then has a chance to defend against the attack. The defend_melee function handles determining if the damage should be taken by comparing the attack value against the defending player’s armor class.

Taking Damage

All players have a damage function that handles keeping the complexity involved minimized to one spot. It takes into account if the player is prone and/or unconscious when attacked.

Death Saves

Death Saves have been implemented as described in the Players Handbook (pgs. 197 & 198) for ALL players whether Hero or Foe. I’ve always played this way because it gives that chance for a Foe to sneak off on a successful series of rolls when the party isn’t looking . That makes for a fun comeback at a later time with a good backstory.

Winning the Encounter

In the earlier game, the first touch ended the game. The goal of this simulation is to have players from only one party survive melee. Each player must choose a an opposing party player to attack, enter into melee, and melee continues until only one of those players is left alive. When there are only players from one party left standing, that party wins.

Let’s Run One

$ ./gen_encounter.py
For the Heroes:
  Quarion Sylfir
  Luric Wranwynn
Against:
  Skeleton
  Skeleton
  Skeleton
The winning party was: Heroes in 13 rounds.
The surviving Heroes members:
Quarion Sylfir
Luric Wranwynn

Okay, it was really a dozen. It’s getting harder to show heroes winning! Could having one more skeleton than the number of party members skew the numbers this much? Could be. To see everything that’s going on, the debug_ind should be turned on. It will dump everything I could think of to output. That can be found on line 15 of gen_encounter.py.

from InvokePSQL import InvokePSQL
from PlayerCharacter import PlayerCharacter
from Foe import Foe
from Encounter import Encounter
from Trace_it import Trace_it


t = Trace_it("encounter")

with t.tracer.span(name='root'):

    db = InvokePSQL()
    Heroes = []
    Opponents = []
    debug_ind = 0
    with t.tracer.span(name='heroes_setup'):
...

Change that value to 1 and all that wonderful info will come spilling out the next time the script is run.

Well, that was a fair amount of work to show off. Reviewing the results with a consistent Hero party would make it a lot easier to investigate if things are working as expected. Right now, all of these parties are being randomly generated. That’ll have to be another post. 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.