Skip to content

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

  • by

Part 15: Got the time?

Using an open source tracing library, called opencensus-python, can help to break down the performance of the different parts of the simulation. Getting started is pretty easy to do and this post will describe that.

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.  

As each part of the simulation gets added in, it’s helpful to see how much time is being spent in each. Here are a couple of tools to visualize that.

Opencensus-python is a library that can be used to export application metrics and distributed traces. Spans are defined in the code and then the data is exported to a backend data store and presentation layer. “A Span represents a single operation within a trace. Spans can be nested to form a trace tree. Often, a trace contains a root span that describes the end-to-end latency and, optionally, one or more sub-spans for its sub-operations.”

Exported traces are consumed by Jaeger, which is a distributed tracing system released as open source by Uber Technologies. It is used for monitoring and troubleshooting microservices-based distributed systems. There is an “all-in-one” docker image that can be easily implemented.

Picking up on the game of Tag that was implemented in part 14, I’d like to see how long an encounter lasts and what it was doing all that time. To do that, a new docker container definition giving new traces a place to be stored is added to the docker-compose.yml file.

jaeger:
    image: jaegertracing/all-in-one:latest
    container_name: jaeger
    ports:
      - "5775:5775/udp"
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
      - "16686:16686"
      - "14268:14268"
      - "9411:9411"

Once that container is started up, changing the Python code to feed it traces becomes pretty easy. Trace_it.py shows how to make a simple class that defines a trace

from opencensus.trace import samplers
from opencensus.trace.tracer import Tracer
from opencensus.ext.jaeger.trace_exporter import JaegerExporter

class Trace_it(object):
    def __init__(self, trace_name ):
        sampler = samplers.AlwaysOnSampler()

        je = JaegerExporter(
            service_name=trace_name,
            host_name="localhost",
            agent_port=6831,
            endpoint="/api/traces")

        self.tracer = Tracer(sampler=sampler, exporter=je)

The Trace_it class, when given a trace_name, will start a trace that samples all executions and sends the traces to the port where the Jaeger container is listening.

Here’s a look at the gen_encounter.py script from the last post with the trace and spans defined.

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'):
        with t.tracer.span(name='hero_1_setup'):
            Heroes.append(PlayerCharacter(db, debug_ind=debug_ind))
        with t.tracer.span(name='hero_2_setup'):
            Heroes.append(PlayerCharacter(db, debug_ind=debug_ind))
    with t.tracer.span(name='opponents_setup'):
        with t.tracer.span(name='opponent_1_setup'):
            Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=debug_ind))
        with t.tracer.span(name='opponent_2_setup'):
            Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=debug_ind))
        with t.tracer.span(name='opponent_3_setup'):
            Opponents.append(Foe(db, foe_candidate='Skeleton', debug_ind=debug_ind))
    print(f"For the Heroes:")
    for Hero in Heroes:
        print(f"  {Hero.get_name()}")
    print(f"Against:")
    for Opponent in Opponents:
        print(f"  {Opponent.get_name()}")
    e1 = Encounter(Heroes, Opponents, debug_ind=0, tracer=t.tracer)
    print(f"The winning party was: {e1.winning_list_name} in {e1.round} rounds.")
    print(f"The surviving {e1.winning_list_name} members:")
    for i in range(len(e1.winning_list)):
        if e1.winning_list[i].alive:
            print(f'{e1.winning_list[i].get_name()}')

Defining a simple span consists of giving a label for the span and then indenting the code that will be timed for that span. For a simple example, the Encounter class has also been updated, adding span definitions for some of the major methods.

$ python gen_encounter.py
For the Heroes:
  Firenze
  Tordek Balderk
Against:
  Skeleton
  Skeleton
  Skeleton
The winning party was: Heroes in 4 rounds.
The surviving Heroes members:
Firenze
Tordek Balderk

Running the script returns the same type of output as earlier executions, but browsing to http://localhost:16686/search and clicking on “Find Traces” button at the bottom of the left frame will show traces that have been gathered since starting the docker container.

Clicking on the “57 Spans” label (your number may vary) will switch to the trace timeline.

This view will show how long each span of the encounter took. As more encounters are run at once and their complexity increases, it will be interesting to see how the execution times break down.

With some tracing in place, the next step is to up the complexity of the game and see what happens. Next time, the melee and ranged weapon attacks will make their way into this mix. 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.