Close

Bringing APEX and PL/SQL to D&D

While there are plenty of opportunities in the real world to use APEX; I felt the need to bring APEX into my gaming. The idea started with a conversation in mid-game about some tactical decisions in a game of Dungeons & Dragons. That is, how should we go about tackling an upcoming combat. After awhile it turned into some meta-gaming of the mathematical implications of various options. Shortly thereafter, I said, “We don’t have to guess. We can just do the math and find out.”

I will be discussing some game mechanics in this article. For the non-gamers though, I will, hopefully, be showing features you can apply in other solutions including real-world applications. The demo application is just two regions. An Interactive Grid to do data entry and chart region to plot curves defined by the grid. The formulas used in this application are, obviously, for calculating damage in the game; but the same code structure can be used for other types of on-the-fly charting and comparisons.

When you start a new session you’ll be shown a set of sample data and a blank chart. These rows are protected in the demo app as example data for everyone to use as guides.

You can copy them and then plot charts on your copies or hide them and make your own. Here I’m comparing a simple attack with no modifiers. I compare the effects of having Advantage or Disadvantage on my average damage output for a single attack against various targets.

For those unfamiliar with the D&D jargon… The attack and damage results are determined by dice. D&D dice include standard 6-sided dice (d6); but also include d4, d8, d10, d12, d20, and d100. Where “d” stands for dice, and the number is number of faces on the die with values going from 1 to the number of faces. Thus a d12 will roll values from 1 to 12, a d4 will roll values 1 to 4, and so on. Multiple die rolls are indicated by prefixing the “d” with the number of times you roll that particular die. So 3d6 would mean rolling a 6-sided die 3 times and summing the results, giving a range of 3 to 18. The damage field can hold a formula of multiple types of die rolls to define attack types, special abilities or other modifiers. For example, “2d6+2d8” would mean rolling a 6-sided die and an 8-sided die twice each, summing the results of all four rolls for a range of 4 to 28.

In addition to the dice themselves you can apply modifiers which are typically just integer addition or subtraction as well as some abilities allowing you to roll more than once and choose between the die results. I’ll go in deeper into the specifics of each data column and the game rules later but first, let’s look at how the app and the code behind it work.

Application Overview

The data for this application is contained within a singled table with columns corresponding to each column seen in the Interactive Grid (IG) plus a few hidden columns to differentiate sessions so two people can use the app at the same time and keep their data distinct. The IG itself is sourced from the table with a where clause restricting each user to seeing only their own session data as well as the sample data.

I use the Allowed Row Operations Column feature of the IG to protect the 5 rows of sample data from updates or deletes so it is always available for every user. A dynamic action on the Save event for the grid triggers a refresh of the chart.

The chart is where things get more interesting. The chart is a simple line chart where each series is derived from rows in the grid with the Chart column checked. A simple recursive query generates armor class values from 1 to 39 (which should be more than enough to cover targets a character might encounter, if not, it would be a simple matter to extend the range in the query.)

Then, for each target value, call a function to generate the expected damage for a given row’s attack criteria.

WITH
    targets
    AS
        (    SELECT LEVEL - 1 ac
               FROM DUAL
         CONNECT BY LEVEL <= 40)
SELECT ddc.calculation_name,
       t.ac,
       demodice.damage_calculator(
           p_base_damage          => ddc.damage,
           p_hit_bonus            => ddc.attack_modifier,
           p_damage_bonus         => ddc.damage_modifier,
           p_ac                   => t.ac,
           p_advantage            => ddc.advantage_disadvantage,
           p_lucky                => ddc.lucky_feature,
           p_critical_threshold   => ddc.critical_threshold,
           p_calc_type            => ddc.calculation_type) d
  FROM demo_damage_calculations ddc CROSS JOIN targets t
  WHERE ddc.chart='Y'
    AND (ddc.owner='SDSDEMO' or ddc.session_id = APEX_CUSTOM_AUTH.GET_SESSION_ID)

The damage calculator takes into account all of the options the user has selected for their attack. In addition, it will factor in rolls of “1” which are an automatic miss as well as critical hits automatically hitting for extra dice damage. For options where an attack has advantage and the character has the lucky feature, there are 8000 possible combinations of attack rolls because you have 3 possible rolls of a d20 (two for advantage or disadvantage, and a third for lucky which may or may not apply, depending on the first two dice; but is still factored in to the combinatorics.)

Inside the package, I don’t do a Monte Carlo simulation of attacks, I use an exhaustive evaluation of every possible roll to generate the expected success rate and apply that to the damage type chosen.

For example, if you have a 50% chance of hitting a particular armor class and you would deal 8 points of damage. Then your expected damage per attack would be 4 points (8 * 0.5). If you have a 75% chance of hitting your expected damage would be 6 points (8 * 0.75). This accuracy factor is applied the same regardless of the damage calculation type. The average damage might be 8, but the maximum damage could be 12 with a minimum of 4. The expected results when dealing maximum damage would then be 6 and 8 (for 50% and 75% respectively) or for minimum damage, 2 and 3 respectively.

In order to determine the likelihood of success for a given armor class, I use a pre-calculated set of results. Which are commented within the code. For example, if you are attacking with disadvantage but have the lucky feat, I don’t generate 8000 rolls, instead I use the following facts about the distribution of attack rolls.

With two disadvantage dice and one luck die there are 58 ways to roll a 1, 814 ways for 2, 770 ways for 3, 726 ways for a 4… 22 ways for 20.
That is 58 for 1, 814 for 2 and then each higher roll has 44 fewer ways (1 and then for n 2..20 (902-44n).

Using these, I multiply the results of each final attack roll (1 to 20) by the number of times each result would occur, summing them as I go to produce the total damage that would be dealt across all 8000 possibilities. Then divide by 8000 to get the mean expected results against that target armor class.

v_result := roll_vs_ac(1) * 58;

FOR i IN 2 .. 20
LOOP
    v_result := v_result + roll_vs_ac(i) * (902 - (44 * i));
END LOOP;

v_result := v_result / 8000;

I follow the same mathematical outline for using one die (20 possible rolls) or two dice (400 possible rolls) as I do with three dice (8000 possible rolls). Compare the roll to the ac, add it’s damage contribution to the total, then divide by the total number of possible rolls.

You can try the application yourself here.

I have scripts to create the table, sample data, package, and APEX application on GitHub here.

Game Mechanics

Damage Dice

The majority of damage is determined by a set of die rolls. You may note a “g” used in some of the sample data. The “g” is just like a “d”, except it means you get to reroll a 1 or 2 on each die. Thus giving you a second-chance to get a higher result. This is not standard D&D notation, but it made for an easy means noting the special rule. So 2g6 would mean rolling a 6-sided die twice. If either or both rolls are a 1 or 2, you can reroll that die one time and keep the second roll (even if you reroll a 2 into 1, you keep the 1.) In game terms, this effect is from “Great Weapon Fighting”, hence the “g” abbreviation.

Attack

The Attack column is used to define a modifier to your attack rolls, this could be a bonus, a penalty, or as in the test example, a modifier of 0. Attacks are determined by rolling a d20, applying the attack modifier and comparing that to your target’s defensive value (called Armor Class, or AC for short). If your result is equal to or higher than your target’s AC, you hit, if it’s lower, you miss. D&D includes two special rules, a 1 on your attack roll always misses, no matter how good your attack modifier is or how low your target’s AC. On the other hand, a 20 always hits; again, regardless of any penalties you have or how high your target’s AC might be.

Damage

The Damage column is a modifier applied after the damage rolls are determined. While addition of constants is allowed in the Damage column, the modifiers are generally kept separate as some game effects (such as Critical Hits) use the unmodified rolls.

Critical Threshold

A Critical Hit means you automatically hit and you double the damage dice. For most characters, you only get a critical hit on an unmodified attack roll of 20. Some though, extend the range of their critical hits, getting them on a 19 or lower.

Advantage

In addition to your numeric modifiers, if you attack with Advantage it means you roll the d20 two times and use the higher of the two results, thus increasing the likelihood of success. Conversely, Disadvantage means you roll twice and take the lower of the two results, thus making failure more likely.

Calculation Type

You can choose to calculate the maximum, minimum, or average damage. This can be a little confusing in the chart, because the max/min/avg result is for the damage dice themselves, not the accuracy of your attacks. If you miss, you deal 0 damage. If you hit, then your calculation type will factor in. So, the results do still trend downward as the target’s AC improves and you hit less often.

Lucky

On top of Advantage and Disadvantage some characters are just plain Lucky which means they can reroll a 1 on any d20 roll (but not if the reroll is also a 1.)
For those familiar with D&D, this would most commonly be due to the Halfling feature, not the Feat; both of which are, unfortunately, named Lucky.

Chart

The chart field simply determines which rows to include in the chart above.

Pulling them all together, in the chart below, Basil is high-level halfling archer with a magical bow. He has a great bonus to attack, and using his sharpshooter has an impressive +20 damage bonus. He’s also lucky, and frequently able to attack with advantage; but he only uses a single d6 for his damage dice.
Enos, is a barbarian/warlock. When he activates all of his abilities, channeling his magic through his weapon, his damage explodes with many dice. He’s easily at the top of the chart for lower target armor classes, but as difficulty hitting increases, Basil catches up and has a higher expected damage output against AC of 21 to 31. Higher than that and it’s so hard to hit the target they are about the same but Enos has a tiny edge just because of the volume of dice he gets to roll. Shyne is another warlock but lower level, with correspondingly lower modifiers. When she casts her spell, she’s at the bottom of the chart.

1 thought on “Bringing APEX and PL/SQL to D&D

Leave a Reply