Skip to content

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

  • by

Part 13: Surveying the Field

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.  At this point, the code has the ability to make legions of combatants. The encounters between player characters and their foes need a battlefield on which to take place. In this post, a battlefield gets built and the logic to place combatants gets defined.

The battlefield that will be used for the simulation will be a square sized by a number of 5 foot by 5 foot segments. The number per column will be defined on the init of the Python Encounter class and defaults to 100. The Encounter class will handle:

  • where the encounter happens
  • who is involved
  • the order of the rounds and turns involved
  • the statistics around the encounter (who wins, how they won, how long it took, etc)
  • the statistics will be sent to the logger for consumption (currently stdout)

Field Sector

Hey Bro, got any snacks?

Each 5 foot by 5 foot sector of the battlefield has an instance of a class associated with it that holds things like:

  • the terrain: is it Normal or Difficult
  • the lighting: is it Bright, Normal, Dim, or Dark
  • whether that particular sector is occupied and by whom

The battlefield is a python list that contains instances of the FieldSector class. When that list is initialized, terrain and lighting will get set. That way if there are any particular spots in the battlefield that are difficult terrain, they can be set up from the get go. Other effects that have a range and duration, like the Darkness spell, can be handled for just the sectors affected for the duration.

Roll for Initiative

When the Encounter class is initialized, two lists of combatants – Heroes and Opponents – are provided. Just like in a real D&D encounter, each combatant rolls for initiative to establish an order for the action. As each gets added to the Initiative list their placement on the battlefield happens.

Character Placement

Where the combatants get placed at the beginning of the encounter has to be consistent since there will be multiple executions of the same encounter. Since the size of the parties can vary it would seem that starting on opposite sides in the middle and placing the next to either side of their last ally would make for the fairest plan.

Loop it

Then the action begins. For this posts example, the idea will be to init the battlefield, place the combatants in the right spots, and loop over the initiative order a few times calculating the distance for each character to their opponents. Eventually, this will be expanded to “ask” each character what their actions are and execute them until only characters from one party remains.

Give it a Spin

Here’s a short script that should build the battlefield, set initiative, place the combatants, and loop through the order 3 times .

#!/usr/bin/env python
# gen_battlefield.py

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

db = InvokePSQL()

Heroes = []
Opponents = []
for i in range(3):
    Heroes.append(PlayerCharacter(db, debugInd=0))
for i in range(2):
    Opponents.append(Foe(db, foeCandidate='Skeleton', debugInd=0))
e1 = Encounter(Heroes, Opponents)
print("Initiative List:")
for i in e1.initiative:
    print(f'srclist: {i[0]}[{i[1]}] perc: {i[2]} ini: {i[3]} '
          f'mapx: {i[4]} mapy: {i[5]}')
print("Occupied field Sectors:")
for x in range(e1.field_size * e1.field_size):
    if (e1.field_map[x].occupied):
        a, b = e1.getGridPosition(x)
        print(f"list position:[{x}] Grid location: [{a}][{b}] "
              f"Occupied by: {e1.field_map[x].occupiedBy}"
              f"[{e1.field_map[x].occupiedByIndex}]")
$ chmod +x gen_battlefield.py
$ ./gen_battlefield.py
Round: 1 Turn: 0 Name: Bryseis Coilbone Grid Pos: [1][49]
dist: 490.0 Opponents 0
dist: 490.02550954006466 Opponents 1
Round: 1 Turn: 1 Name: Harbek Balderk Grid Pos: [1][48]
dist: 490.0 Opponents 1
dist: 490.02550954006466 Opponents 0
Round: 1 Turn: 2 Name: Casaraine Grid Pos: [1][50]
dist: 490.02550954006466 Opponents 0
dist: 490.1020301937138 Opponents 1
Round: 1 Turn: 3 Name: Skeleton Grid Pos: [99][49]
dist: 490.0 Heroes 0
dist: 490.02550954006466 Heroes 1
dist: 490.02550954006466 Heroes 2
Round: 1 Turn: 4 Name: Skeleton Grid Pos: [99][48]
dist: 490.0 Heroes 1
dist: 490.02550954006466 Heroes 0
dist: 490.1020301937138 Heroes 2
Round: 2 Turn: 0 Name: Bryseis Coilbone Grid Pos: [1][49]
dist: 490.0 Opponents 0
dist: 490.02550954006466 Opponents 1
Round: 2 Turn: 1 Name: Harbek Balderk Grid Pos: [1][48]
dist: 490.0 Opponents 1
dist: 490.02550954006466 Opponents 0
Round: 2 Turn: 2 Name: Casaraine Grid Pos: [1][50]
dist: 490.02550954006466 Opponents 0
dist: 490.1020301937138 Opponents 1
Round: 2 Turn: 3 Name: Skeleton Grid Pos: [99][49]
dist: 490.0 Heroes 0
dist: 490.02550954006466 Heroes 1
dist: 490.02550954006466 Heroes 2
Round: 2 Turn: 4 Name: Skeleton Grid Pos: [99][48]
dist: 490.0 Heroes 1
dist: 490.02550954006466 Heroes 0
dist: 490.1020301937138 Heroes 2
Round: 3 Turn: 0 Name: Bryseis Coilbone Grid Pos: [1][49]
dist: 490.0 Opponents 0
dist: 490.02550954006466 Opponents 1
Round: 3 Turn: 1 Name: Harbek Balderk Grid Pos: [1][48]
dist: 490.0 Opponents 1
dist: 490.02550954006466 Opponents 0
Round: 3 Turn: 2 Name: Casaraine Grid Pos: [1][50]
dist: 490.02550954006466 Opponents 0
dist: 490.1020301937138 Opponents 1
Round: 3 Turn: 3 Name: Skeleton Grid Pos: [99][49]
dist: 490.0 Heroes 0
dist: 490.02550954006466 Heroes 1
dist: 490.02550954006466 Heroes 2
Round: 3 Turn: 4 Name: Skeleton Grid Pos: [99][48]
dist: 490.0 Heroes 1
dist: 490.02550954006466 Heroes 0
dist: 490.1020301937138 Heroes 2
Initiative List:
srclist: Heroes[0] perc: True ini: 12 mapx: 1 mapy: 49
srclist: Heroes[1] perc: False ini: 10 mapx: 1 mapy: 48
srclist: Heroes[2] perc: True ini: 9 mapx: 1 mapy: 50
srclist: Opponents[0] perc: False ini: 9 mapx: 99 mapy: 49
srclist: Opponents[1] perc: True ini: 6 mapx: 99 mapy: 48
Occupied field Sectors:
list position:[148] Grid location: [1][48] Occupied by: Heroes[1]
list position:[149] Grid location: [1][49] Occupied by: Heroes[0]
list position:[150] Grid location: [1][50] Occupied by: Heroes[2]
list position:[9948] Grid location: [99][48] Occupied by: Opponents[1]
list position:[9949] Grid location: [99][49] Occupied by: Opponents[0]

The Initiative List part of the output above shows that the order appears to work. Notice how the “ini” values are in descending order (12,10,9, 6). The placement of the combatants looks correct based on the “Occupied field Sectors” output. All Heroes are conceptually in the same side of the field, Opponents are on the other and each has the right number of their party members on either side. The iteration through each round goes by initiative order, computing the distance to the opponents sorting from closest to farthest.

The next post will put together a game of tag for our combatants.

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.