Dungeons and Dice: Is +1d4 or Advantage Better?

Language: Python — Time: 90 minutes with research
Full Code on Gitfront.iohttps://github.com/cswartzell

Another problem inspired by a Reddit thread: In Dungeons and Dragons nearly all actions are assigned a “DC” (Difficulty Class), which is a target number you want to meet or exceed when rolling a single 20 sided die (1d20) to succeed. For instance, “roll better than 15 to climb this wall”. There are certain actions players can take can aid their fellow party members, giving them assistance for these rolls. The Cleric spell Guidance, for instance, adds an additional 1d4 to a players next d20 roll while Enhance Ability grants “Advantage” on a roll which allows a player to roll two d20 and keep the higher result. Both of these options give a player a minor edge, but in different ways. Given the choice, which is statistically superior?

A pretty simple probability problem, and a good chance to play around with the matplotlib library in Python. We can simply generate all outcomes (1d20+1d4, and max(2d20)), calculate the probabilities of each combination, create a running total for each result showing “chance of rolling at least X”, and graph the result. Matplotlib allows us to create a clean, simple graph in just a few lines of code:

The answer ends up being quite surprising: It depends on the “DC” of the roll we are intending!

Adding 1d4 mostly just adds a simple offset to a linear probability, while advantage changes the probability to being nonlinear. Graphing the two, we see the lines cross each other multiple times, though just barely on the left.

On the low end, if we must avoid rolling a one to succeed in some challenge, then obviously we should take the added die as the lowest roll for 1d20+1d4 is 2, while there is still a slim possibility of 2d20 each showing a 1 (a 1 in 400 chance). Above this minimum, rolling 2d20 with Advantage gains an edge for some time, and results in the range 3-17 are significantly more likely.

For middle values we see that the Advantaged roll is significantly more likely to roll “at least” those targets. At its peak, rolling with Advantage gives us a 75% chance of getting at least 11, while 1d20+1d4 has only a 62.5% chance to be at least as much.

At 18 the lines very nearly cross again, though Advantage retains an edge, there being 27.75% chance of rolling at least 18 with Advantage compared with 27.50% if rolling 1d20+1d4. After a DC target of 18 the added 1d4 takes the lead again, and is more likely to give you a higher total. Significantly, the added d4 allows a player to roll above 20 as the combined results go up to 24. We now see this is actually the reason why the added die is less favorable for most middle target numbers: there are more total results so the probability of any specific result is commensurately reduced.

So, which to use? We see now this is dependent on the target DC a value known only to the Dungeon Master. Players don’t have perfect information when playing and can only make an informed guess. Experienced players may have a sense (10 is pretty standard, 15 is a hard task, 20 is quite a challenge), and can perhaps game the system by estimating what the DM has in mind, but the difference is fairly slight either way.

More Math:
2d20 with advantage:
Mean: 13.82
Deviation: 4.71

1d20+1d4:
Mean: 13 (simply: mean 1d20 + mean 1d4 = 10.5 + 2.5)
Deviation: 5.87

Dungeons & Dragons & Diagonal Movement

Language: Python —
Full Code on Gitfront.iohttps://github.com/cswartzell

A friend of mine is working on a Unity project based on faithfully recreating tabletop RPG rules, and asked for a hand coding the rules for movement. In Dungeons and Dragons character movement is done on a grid of squares where each square represents 5’ x 5’ of space. Traveling orthogonally from one square to another simply takes 5’ of movement. However, the rule for diagonal movements are… unusual.

A character taking their first diagonal step in a turn is charged as having moved 5’. This makes all 8 squares surrounding the starting position an equal 5’ away. Afterwards, any second diagonal move during a turn takes 10’ of movement even if not contiguous or in the same direction as the first diagonal step. The pattern then repeats; 5’, 10’, 5’, 10’… simple enough and a good compromise for efficient movement on a grid. Plotting say 30’ of movement using this system starts to roughly approximates a circle.

As a coding challenge, how do we plot a characters movement? How do we draw a movement “circle” indicating grid spaces they can reach within their movement distance? How far away is a target from a character, assuming the most direct route using these movement rules?

As this project is Unity based, some simple solutions come to mind. My friend easily created a coordinate based grid system, and can arbitrarily assign scales to the world. We could simply take an actual measurement from the center of one grid position to another to determine their distance by rounding to the nearest 5’. Will this accurately model the rules though? Hard to say, but my intuition is it will accumulate errors, or rather “corrections” that deviate from the more approximate game rules vs real measure. Similarly, we could treat each diagonal move as 7.5’ and, again with rounding, come up with a decent approximation. While fine choices for a game these methods may not exactly match the game rules.

Instead, we need to come with an algorithm for determining optimum grid movement given the movement rules as written. The most efficient movement is orthogonal; moving in a straight path along a row or column always costs us 5’ per square. We want to travel in an orthogonal path toward our target for as much distance as possible. For any square not in our current row or column we will have to alter our direction at some point. However, unlike in real life, the straight line from the origin to the target may not be the shortest route if it costs an extra 10’ diagonal step we could have avoided.

The least efficient movement would be L shaped, barring intentionally indirect paths. Using this method moving 4 squares North followed by 4 squares East would be 8 steps. Let’s start using coordinates to notate this; (0,0)->(4,4) at 5’ step size would be a “Manhattan Distance” of 40’. We could instead make just 4 diagonal movements NE to go directly toward the target. At a cost of 5’, then 10’, 5’ and 10’ again, we would arrive at the same square using only 30’ of movement. Diagonal movement is the more efficient than L shaped movement.

So, we want to travel in a straight line as much as possible yet may need to get over a few rows or columns first in order to do this. Combining these two ideas we see that we should move directly diagonally, along the shorter leg of the L, toward either the row or column that lets us make the rest of our movement in a straight line.

After some thinking and tinkering I hit upon the algorithm that solves this. Let’s call East-West movement row based and North-South column based. To go from our characters origin (0,0) to our target (tgt_row, tgt_col), we could travel the L shaped Manhattan Distance of (5’ * tgt_row) + (5’ * tgt_col) less some savings by moving diagonally. Specifically, we save 5’ of movement on the Manhattan route per odd diagonal step we take. We need to take diagonal steps for the minimum between tgt_row and tgt_col to get into a lane where the remainder of our movement is straight. Putting it all together, the most efficient route from one space to another using these rules is given by:

#Our target (x,y) is the absolute value of the offset from the characters position to the target’s step_size = 5 tgt_x = abs(char_row - tgt_row) tgt_y = abs(char_col - tgt_col) distance = step_size * (tgt_x + tgt_y - ceil(min(tgt_x, tgt_y)/2))

Using this as our basis, we can draw out a grid for movement costs on an arbitrary grid:

Limiting the output to our characters movement speed, we can correctly highlight the spaces our character can reach in a single move. We can also assign a target to a grid position (noted here by being underlined, and use the same formula to calculate the distance to a target:

Neat.