After the terrible code I wrote in a previous post I wanted to see if a) if I could still write less ugly code and b) what the opposed roll “resistance table”1 would look like for other d100 systems.
I therefore set out to calculate opposed rolls for the four major extant d100 systems that I know about: Basic Roleplaying (Chaosium), Call of Cthulhu 7th ed (Chaosium), Mythras (Design Mechanism), and OpenQuest (D101 Games).2
Opposed Roll Tables
Nearly all percentile dice systems define a character’s skill level as a percentile score. The player succeeds if the roll is at or below this score, and fails if the roll is above this score. That said, there are numerous ways to define levels of success (or failure). There are also numerous ways to define what happens in an opposed test, when two parties (usually a player and GM) roll dice against their respective skill levels to see who wins.
We will consider four systems, and how they defined opposed tests. I will also present the chances of winning an opposed contest based on the skill levels of the two parties.
Basic Roleplaying
The 2008 “Big Gold Book”, on which the 2023-2024 rules are based, defines two levels of success beyond the simple pass/fail:
- Critical Success is 1/20th of the character’s skill level.
- Special Success is 1/5th of the character’s skill level.
In addition to regular Failure, it also defines a Fumble: the character not merely failed but messed up bad. In BRP, if the die rolls is close to (1)00 – 1/20th of the failure chance close, to be exact, the result is a Fumble.
In an opposed test, both parties roll dice and see who gained the greater level of success: Critical beats all, Special beats Regular, Success beats Failure, Failure beats Fumble. If both parties have the same level of success, the one with the higher die roll wins.
The 2008 version apparently used a comparison of skill not die rolls. Apparently the recent (2023?) version used a comparison of die rolls. At least the diagonal is 50.5% now.
Pl. | 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 50.50 | 41.50 | 33.55 | 25.62 | 19.66 | 13.74 | 9.78 | 5.87 | 3.91 | 2.01 | 1.90 |
10 | 59.40 | 50.50 | 41.48 | 32.60 | 25.64 | 18.85 | 13.97 | 9.22 | 6.34 | 3.60 | 2.70 |
20 | 67.25 | 59.40 | 50.50 | 40.68 | 32.66 | 24.93 | 19.00 | 13.36 | 9.52 | 5.97 | 4.28 |
30 | 75.07 | 68.15 | 60.19 | 50.50 | 41.32 | 32.54 | 25.45 | 18.76 | 13.76 | 9.16 | 6.43 |
40 | 80.93 | 74.98 | 68.08 | 59.55 | 50.50 | 40.80 | 32.66 | 25.05 | 19.00 | 13.48 | 9.78 |
50 | 86.74 | 81.64 | 75.68 | 68.20 | 60.07 | 50.50 | 41.20 | 32.54 | 25.33 | 18.76 | 14.01 |
60 | 90.60 | 86.42 | 81.48 | 75.16 | 68.08 | 59.67 | 50.50 | 40.92 | 32.66 | 25.17 | 19.45 |
70 | 94.40 | 91.06 | 86.99 | 81.72 | 75.56 | 68.20 | 59.95 | 50.50 | 41.08 | 32.54 | 25.76 |
80 | 96.26 | 93.84 | 90.70 | 86.59 | 81.48 | 75.28 | 68.08 | 59.79 | 50.50 | 41.04 | 33.29 |
90 | 98.05 | 96.47 | 94.12 | 91.06 | 86.87 | 81.72 | 75.44 | 68.20 | 59.83 | 50.50 | 41.68 |
100 | 98.12 | 97.32 | 95.74 | 93.70 | 90.48 | 86.37 | 81.06 | 74.87 | 67.47 | 59.20 | 50.50 |
Call of Cthulhu 7
Among many controversial3 changes, 7th Edition of Call of Cthulhu changed character attributes from the usual 1-20 (well, 3-18) range to percentiles. (It’s just multiplying by 5, guys.) This also made the resistance table1 obsolete, replacing it with opposed rolls. (Which gets tricky when one is holding a door against a huge monster, but they had a solution for that.)
Another controversial change was this versions introduction of multiple levels of success:
- Critical Success, defined as a natural 01.
- Extreme Success, defined as less than or equal to a fifth of your skill level, rounded down.
- Hard Success, defined as less than or equal to half your skill level, rounded down.
- (Ordinary) Success, defined as your skill level or less.
Add to that the usual Failure (more than your skill level) and Fumble (at 96 or above).
The winner of an opposed skill test therefore has a higher success level than their opponent. If the two contestants’ success levels are the same, the one with the higher skill wins. If their skills are tied, the rules suggest the two combatants roll again.
When I first wrote a Ruby program to generate this table, I was surprised at how quickly and non-linearly percentages dropped off from the equal skill diagonal. It still looks weird to me, especially the lower skill levels.
Pl. | 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 50.01 | 5.69 | 5.69 | 5.69 | 5.69 | 1.93 | 1.93 | 1.93 | 1.93 | 1.93 | 1.93 |
10 | 94.31 | 50.03 | 12.90 | 12.23 | 11.56 | 7.49 | 6.82 | 6.15 | 5.48 | 4.81 | 4.19 |
20 | 94.31 | 87.10 | 50.02 | 19.66 | 18.30 | 13.94 | 12.58 | 11.22 | 9.86 | 8.50 | 7.24 |
30 | 94.31 | 87.77 | 80.34 | 50.03 | 25.04 | 20.39 | 18.34 | 16.29 | 14.24 | 12.19 | 10.29 |
40 | 94.31 | 88.44 | 81.70 | 74.96 | 50.02 | 26.84 | 24.10 | 21.36 | 18.62 | 15.88 | 13.34 |
50 | 98.07 | 92.51 | 86.06 | 79.61 | 73.16 | 50.03 | 29.90 | 26.47 | 23.04 | 19.61 | 16.43 |
60 | 98.07 | 93.18 | 87.42 | 81.66 | 75.90 | 70.10 | 50.02 | 31.54 | 27.42 | 23.30 | 19.48 |
70 | 98.07 | 93.85 | 88.78 | 83.71 | 78.64 | 73.53 | 68.46 | 50.03 | 31.80 | 26.99 | 22.53 |
80 | 98.07 | 94.52 | 90.14 | 85.76 | 81.38 | 76.96 | 72.58 | 68.20 | 50.02 | 30.68 | 25.58 |
90 | 98.07 | 95.19 | 91.50 | 87.81 | 84.12 | 80.39 | 76.70 | 73.01 | 69.32 | 50.03 | 28.63 |
100 | 98.07 | 95.81 | 92.76 | 89.71 | 86.66 | 83.57 | 80.52 | 77.47 | 74.42 | 71.37 | 50.02 |
Mythras
Mythras defines two levels of success: Critical (1/10 of skill) and Normal (at or under skill). Unlike the others, a roll of 5 or less always succeeds, and a roll of 96 or more always fails. It also has Fumbles, defined as 99 or 00 unless skill is high enough. Again, the one with the higher level of success wins, and two characters who succeed compare rolls to see which is higher.
Unlike the others, Mythras insists that if both parties have an ordinary failure, neither party wins. Thus the diagonal is nowhere near 50% until higher skill levels (80% or better).
Pl. | 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 4.90 | 4.70 | 4.25 | 3.81 | 3.38 | 2.96 | 2.55 | 2.14 | 1.73 | 1.32 | 1.11 |
10 | 9.85 | 9.55 | 8.55 | 7.56 | 6.58 | 5.61 | 4.65 | 3.70 | 2.76 | 1.83 | 1.36 |
20 | 19.79 | 19.54 | 18.10 | 16.10 | 14.11 | 12.13 | 10.16 | 8.20 | 6.25 | 4.31 | 3.28 |
30 | 29.72 | 29.52 | 28.09 | 25.65 | 22.65 | 19.66 | 16.68 | 13.71 | 10.75 | 7.80 | 6.21 |
40 | 39.64 | 39.49 | 38.07 | 35.64 | 32.20 | 28.20 | 24.21 | 20.23 | 16.26 | 12.30 | 10.15 |
50 | 49.55 | 49.45 | 48.04 | 45.62 | 42.19 | 37.75 | 32.75 | 27.76 | 22.78 | 17.81 | 15.10 |
60 | 59.46 | 59.40 | 58.00 | 55.59 | 52.17 | 47.74 | 42.30 | 36.30 | 30.31 | 24.33 | 21.06 |
70 | 69.37 | 69.34 | 67.95 | 65.55 | 62.14 | 57.72 | 52.29 | 45.85 | 38.85 | 31.86 | 28.03 |
80 | 79.28 | 79.27 | 77.89 | 75.50 | 72.10 | 67.69 | 62.27 | 55.84 | 48.40 | 40.40 | 36.01 |
90 | 89.19 | 89.19 | 87.82 | 85.44 | 82.05 | 77.65 | 72.24 | 65.82 | 58.39 | 49.95 | 45.00 |
100 | 94.15 | 94.15 | 92.84 | 90.52 | 87.19 | 82.85 | 77.50 | 71.14 | 63.77 | 55.39 | 50.35 |
OpenQuest 3
In OpenQuest, a Critical is defined as a success where the dice read double digits (11, 22, 33, etc.), and a Fumble is defines as a failure that reads double digits (00, 99, 88, etc.). If two characters have the same level of success or an ordinary failure, they see who came closest to their skill level from either direction. If both fumble, nobody wins.
We saw this one before, and after seeing the ones above I’m surprised how smooth the progression from the 50% diagonal is. Or that we have a 50% diagonal at all.
Pl. | 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 49.95 | 49.40 | 47.15 | 44.09 | 40.22 | 35.54 | 30.05 | 23.75 | 16.64 | 8.72 | 0.00 |
10 | 50.40 | 49.95 | 47.70 | 44.64 | 40.77 | 36.09 | 30.60 | 24.30 | 17.19 | 9.27 | 0.55 |
20 | 52.66 | 52.21 | 50.05 | 46.98 | 43.10 | 38.41 | 32.91 | 26.60 | 19.48 | 11.55 | 2.81 |
30 | 55.73 | 55.28 | 53.12 | 50.14 | 46.25 | 41.55 | 36.04 | 29.72 | 22.59 | 14.65 | 5.89 |
40 | 59.61 | 59.16 | 57.00 | 54.02 | 50.22 | 45.51 | 39.99 | 33.66 | 26.52 | 18.57 | 9.79 |
50 | 64.30 | 63.85 | 61.69 | 58.71 | 54.91 | 50.29 | 44.76 | 38.42 | 31.27 | 23.31 | 14.51 |
60 | 69.80 | 69.35 | 67.19 | 64.21 | 60.41 | 55.79 | 50.35 | 44.00 | 36.84 | 28.87 | 20.05 |
70 | 76.11 | 75.66 | 73.50 | 70.52 | 66.72 | 62.10 | 56.66 | 50.40 | 43.23 | 35.25 | 26.41 |
80 | 83.23 | 82.78 | 80.62 | 77.64 | 73.84 | 69.22 | 63.78 | 57.52 | 50.44 | 42.45 | 33.59 |
90 | 91.16 | 90.71 | 88.55 | 85.57 | 81.77 | 77.15 | 71.71 | 65.45 | 58.37 | 50.47 | 41.59 |
100 | 100.00 | 99.55 | 97.39 | 94.41 | 90.61 | 85.99 | 80.55 | 74.29 | 67.21 | 59.31 | 50.50 |
Program
#!/usr/bin/env lua
-- Status code for a Fumble, defined variously in various rules.
local STATUS_FUMBLE <const> = -1
-- Status code for an ordinary Failure, i.e. > skill and not a Fumble.
local STATUS_FAILURE <const> = 0
-- Status code for an ordinary Success, i.e. <= skill and not a higher level.
local STATUS_SUCCESS <const> = 1
-- Status code for a CoC "hard" success, i.e. <= skill/2
local STATUS_HARD <const> = 2
-- Status code for a CoC "extreme" success, i.e. <= skill/5
local STATUS_EXTREME <const> = 3
-- Status code for a BRP "special" success, i.e. <= skill/5
local STATUS_SPECIAL <const> = 4
-- Status code for a critical success, defined variously in various rules.
local STATUS_CRITICAL <const> = 5
-- What level of success a roll of `p` indicates for a percentile `pct`
-- in Basic Roleplaying (2023)
local function success_level_brp(p, pct)
if p == 1 then
return STATUS_CRITICAL
elseif p == 100 then
return STATUS_FUMBLE
elseif p <= pct then
-- p is between 2 and 99
if p <= pct // 20 then
return STATUS_CRITICAL
elseif p <= pct // 5 then
return STATUS_SPECIAL
else
return STATUS_SUCCESS
end
else
local fumble = 100 - (100 - pct) // 20
if p >= fumble then
return STATUS_FUMBLE
end
return STATUS_FAILURE
end
end
-- Whether player wins on opposed rolls `p` vs `r`
-- with skills `player` vs. `resist`
-- in Basic Roleplaying (2023)
local function is_player_win_brp(p, r, player, resist)
local pstat, rstat =
success_level_brp(p, player),
success_level_brp(r, resist)
return pstat > rstat or (pstat == rstat and p >= r)
end
-- What level of success a roll of `p` indicates for a percentile `pct`
-- in Call of Cthulhu 7th Edition
local function success_level_coc7(p, pct)
if p == 1 then
return STATUS_CRITICAL
elseif p == 100 then
return STATUS_FUMBLE
elseif p <= pct then
-- p is between 2 and 99
if p <= pct // 5 then
return STATUS_EXTREME
elseif p <= pct // 2 then
return STATUS_HARD
else
return STATUS_SUCCESS
end
else
if pct < 50 and p >= 96 then
return STATUS_FUMBLE
else
return STATUS_FAILURE
end
end
end
-- Whether player wins on opposed rolls `p` vs `r`
-- with skills `player` vs. `resist`
-- in Call of Cthulhu 7th Edition
local function is_player_win_coc7(p, r, player, resist)
local pstat, rstat =
success_level_coc7(p, player),
success_level_coc7(r, resist)
if pstat > rstat then
return true
elseif pstat == rstat then
-- According to the CoC7 Quickstart p. 10,
-- "In the case of a draw, the side with the higher
-- skill value wins. If both skills are equal then have
-- both sides roll 1D100, with the lower result winning."
if player > resist then
return true
elseif player == resist then
-- Best approximation to random second roll.
return (p + r) % 2 == 0
end
end
end
-- What level of success a roll of `p` indicates for a percentile `pct`
-- in Mythras
local function success_level_mythras(p, pct)
-- 1 ... 5 is always a success
-- 96 ... 100 is always a failure
if p <= 5 or (p < 96 and p <= pct) then
if p == 1 or p <= pct // 10 then
return STATUS_CRITICAL
else
return STATUS_SUCCESS
end
else
if p == 100 or (pct <= 100 and p == 99) then
return STATUS_FUMBLE
else
return STATUS_FAILURE
end
end
end
-- Whether player wins on opposed rolls `p` vs `r`
-- with skills `player` vs. `resist`
-- in Mythras
local function is_player_win_mythras(p, r, player, resist)
local pstat, rstat =
success_level_mythras(p, player),
success_level_mythras(r, resist)
return pstat >= STATUS_SUCCESS
and (pstat > rstat or (pstat == rstat and p >= r))
end
-- What level of success a roll of `p` indicates for a percentile `pct`
-- in OpenQuest 3rd edition.
local function success_level_oq3(p, pct)
if p % 11 == 0 or p == 100 then
-- roll is doubles: 11, 22, 33, ... 99, 00
if p <= pct then
return STATUS_CRITICAL
else
return STATUS_FUMBLE
end
else
if p <= pct then
return STATUS_SUCCESS
else
return STATUS_FAILURE
end
end
end
-- Whether player wins on opposed rolls `p` vs `r`
-- with skills `player` vs. `resist`
-- in OpenQuest 3rd edition.
local function is_player_win_oq3(p, r, player, resist)
local pstat, rstat =
success_level_oq3(p, player),
success_level_oq3(r, resist)
if pstat > rstat then
return true
elseif pstat == rstat then
if pstat >= STATUS_SUCCESS then
return p >= r
elseif pstat == STATUS_FAILURE then
return p <= r
end
-- STATUS_FUMBLE is never a player win
end
end
-- Figure out probabilities of opposed rolls through brute force
local function opposed_roll(player, resist, pfunc)
local sum = 0
for p = 1, 100 do
for r = 1, 100 do
if pfunc(p, r, player, resist) then
sum = sum + 1
end
end
end
return sum/10000
end
local function format_header(min, max, step)
local buf = { "", " Pl." }
for i = min, max, step do
table.insert(buf, string.format(" %5d ", i))
end
table.insert(buf, "")
return table.concat(buf, "|")
end
local function format_bar(min, max, step)
local buf = { "", ":--:" }
for i = min, max, step do
table.insert(buf, "------:")
end
table.insert(buf, "")
return table.concat(buf, "|")
end
local function format_row(percent, results)
local buf = { "", string.format("%3d ", percent), }
for n = 1, #results do
table.insert(buf, string.format("%6.2f ", results[n] * 100))
end
table.insert(buf, "")
return table.concat(buf, "|")
end
local RULES_LIST <const> = {
{n = "Basic Roleplaying", f = is_player_win_brp},
{n = "Call of Cthulhu 7", f = is_player_win_coc7},
{n = "Mythras", f = is_player_win_mythras},
{n = "OpenQuest 3", f = is_player_win_oq3},
}
local function print_columns(min, max, step)
for i, rules in ipairs(RULES_LIST) do
for p = min, max, step do
for r = min, max, step do
print(string.format("%s\t%d\t%d\t%.4f",
rules.n,
p,
r,
opposed_roll(p, r, rules.f)))
end
end
end
end
local function print_table(min, max, step)
for i, rules in ipairs(RULES_LIST) do
print("###", rules.n, "###")
print(format_header(min, max, step))
print(format_bar(min, max, step))
for p = min, max, step do
local results = {}
for r = min, max, step do
table.insert(results, opposed_roll(p, r, rules.f))
end
print(format_row(p, results))
end
end
end
if arg[1] == '-l' then
print_columns(0, 100, 10)
else
print_table(0, 100, 10)
end
-
An old mechanic for an active attribute to resist another attribute, where attributes are usually in the range 1-20. Basically the equation “50% + (Active - Resist) × 5%” as a table. ↩︎ ↩︎
-
Sorry, Legend (Mongoose), you didn’t make the cut, but I wasn’t sure you were still supported. And sorry, too, French-made RPG that I remember buying but can’t even remember the name of. ↩︎
-
I’m not saying they should have been controversial. Editions 1-6 had changed very little except to add more “stuff”. The changes augmented mechanics that hadn’t changed since the ’80s. But like all fandoms, the two modes of CoC players are “it’s changed so it sucks” and “it’s the same so it sucks”, so Chaosium was doomed either way. ↩︎