NPC Party Dungeon Delving and Wilderness Encounter Simulation
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)