As a reasonable follow-up to single NPC dungeon delving simulation, now we test parties of adventurers.

Strength in Unity, Complementary

Parties are stronger than individual NPC, but without being overpowered. The increase in power is not linear: 5 heroes working together are not 5x more effective than a single hero, but general survival rate is greatly improved. So, here are a few similarities/differences between a single NPC and a party, with regards to mechanics in the coarse simulation layer that I'm currently developing:

  • Healing is applied to all members equally
  • Damage is reduced based on the number (N) of party members by: $latex \frac{N-1}{2N}$
    • N=1: 0%, N=2: 25%, N=3: 33%, etc.
  • XP are divided among party members
  • For skill checks: use the max value among members
  • For skill category checks: for each member calculate the average of the category's skills, and use the max among members

With these in mind, it's clear that it's beneficial to have parties with complementary skills.

Single-Delve Tests Revisited

Here are a few examples that show survival rate in a few party size configurations (1,2,6) when we put a party (of any level) against a dungeon of a similar level.

Lifetime-Delve Tests Revisited

Here are a few examples that show survival rate when we put a lvl 1 party against a series of dungeons of a similar level to the party (dungeons progressively get stronger as party levels up). CR mod is difference of character level to dungeon level, so a crmod of -5 means a level 25 party will tackle a level 20 dungeon.

 

Wilderness Encounters

Adventurers start their adventures typically at cities. They travel, and travel, following well-established routes as much as possible and crossing through other cities, until finally they enter full-on wilderness to get to a dungeon to clear. So, with some quick and not-too-inaccurate math we can figure out that if $latex X$ is the total distance to the dungeon and $latex Y$ is the average city-to-city distance, then the distance off roads will be $latex \text{min}(Y/2,X)$. The split to off-road and on-route is important, as threat on route is significantly smaller.

The tests assume a +-20% in challenge rating compare to the character level, and they utilize the dungeon delving simulation code from the previous post, but with 1-3 encounters only.

These examples show the benefits of being in a party, and they also demonstrate that survival chances drop with larger distances (but the drop decays sharply), and they also drop with higher character levels, as more dangerous encounters get spawned.

Side-effect plot viewer

The generated graphs use several parameter sets, e.g. party size, retreat threshold, etc. It becomes difficult to navigate through the graphs when you want to flip between two arbitrary parameter values. So, instead of researching it further, I wrote a dead simple script that allows "interactive" graph rendering in the cheatiest way: it parses special parameter-encoding filenames, e.g. "wildernesssurvival_retreat2_party5.png", and listens to keypresses. When e.g. 'r' is pressed, I load the file 'wildernesssurvival_retreat3_party5.png', while now if 'p' is pressed, I load 'wildernesssurvival_retreat3_party6.png'. This is actually a lifesaver! And works with all graphs. Here's the code in all its glory:

 

import glob
import sys
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

def run(img_format, params):

    param_ranges = [] # for each param, a list of values
    param_cur_indices = []
    ax = None
    fig = None

    def press(event):
        sys.stdout.flush()
        for k in range(len(params)):
            if event.key.lower() == params[k]:
                off = 1 if event.key == params[k] else len(param_ranges[k])-1
                param_cur_indices[k] = (param_cur_indices[k]+off) % len(param_ranges[k])
                param_cur_values = [ param_ranges[i][param_cur_indices[i]] for i in range(len(params))]
                img=mpimg.imread( img_format.format(*param_cur_values))
                ax.imshow(img)
                fig.canvas.draw()

    all_files = glob.glob(img_format.replace("{}","*"))
    img_format_const_parts = img_format.split('{}')
    for i in range(len(params)):
        """
            To extract number for i-th param, locate the end of the string before and the start of the string after
        """
        values = []
        for j in range(len(all_files)):
            fname = all_files[j].replace('\\','/')
            idx_start = fname.find(img_format_const_parts[i]) + len(img_format_const_parts[i])
            idx_end = fname.find(img_format_const_parts[i+1])
            values.append( int(fname[idx_start:idx_end]))
        values = sorted(list(set(values)))
        param_ranges.append(values)
        
    param_cur_indices = [0] * len(params)
    param_cur_values = [ param_ranges[i][param_cur_indices[i]] for i in range(len(params))]

    plt.close('all')
    plt.ioff()
    img=mpimg.imread( img_format.format(*param_cur_values))

    fig, ax = plt.subplots()
    plt.axis('off')
    fig.canvas.mpl_connect('key_press_event', press)
    ax.imshow(img)
    plt.show(block=True)


if __name__ == '__main__':
    #img_format = 'C:/Users/Babis/Documents/repos/aot/build/apps/aot_v0/imgadvlocreslvl_crmod{}_enc{}_retreat{}.png'
    #params = ['c','e','r']

    #img_format = 'C:/Users/Babis/Documents/repos/aot/build/apps/aot_v0/advlocres_crmod{}_enc{}_retreat{}_party{}.png'
    #params = ['c','e','r','p']

    img_format = 'C:/Users/Babis/Documents/repos/aot/build/apps/aot_v0/wildernesssurvival_retreat{}_party{}.png'
    params = ['r','p']
    run(img_format, params)