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
Basic Roleplaying
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.5% | 40.6% | 32.6% | 24.7% | 18.7% | 12.8% | 8.8% | 4.9% | 3.0% | 1.1% | 0.0% |
10 | 60.3% | 50.5% | 41.4% | 32.5% | 25.4% | 18.5% | 13.5% | 8.5% | 5.5% | 2.6% | 0.5% |
20 | 68.2% | 59.5% | 50.5% | 40.6% | 32.6% | 24.8% | 18.7% | 12.9% | 8.8% | 5.0% | 2.1% |
30 | 76.0% | 68.3% | 60.2% | 50.5% | 41.3% | 32.5% | 25.4% | 18.5% | 13.4% | 8.5% | 4.7% |
40 | 81.9% | 75.2% | 68.2% | 59.6% | 50.5% | 40.8% | 32.6% | 24.9% | 18.7% | 13.0% | 8.2% |
50 | 87.7% | 82.1% | 75.9% | 68.3% | 60.1% | 50.5% | 41.2% | 32.5% | 25.2% | 18.5% | 12.8% |
60 | 91.5% | 87.0% | 81.9% | 75.3% | 68.2% | 59.7% | 50.5% | 40.8% | 32.6% | 25.0% | 18.3% |
70 | 95.3% | 91.8% | 87.6% | 82.1% | 75.8% | 68.3% | 60.0% | 50.5% | 41.1% | 32.5% | 24.9% |
80 | 97.2% | 94.8% | 91.5% | 87.1% | 81.9% | 75.4% | 68.2% | 59.8% | 50.5% | 40.9% | 32.4% |
90 | 99.0% | 97.6% | 95.2% | 91.8% | 87.5% | 82.1% | 75.7% | 68.3% | 59.9% | 50.5% | 40.9% |
100 | 100.0% | 99.6% | 98.1% | 95.7% | 92.2% | 87.8% | 82.3% | 75.8% | 68.4% | 60.0% | 50.5% |
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.)
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.0% | 4.8% | 4.8% | 4.8% | 4.8% | 4.8% | 4.8% | 4.8% | 4.8% | 4.8% | 0.9% |
10 | 95.2% | 50.0% | 12.9% | 12.3% | 11.7% | 11.0% | 10.4% | 9.7% | 9.1% | 8.5% | 4.5% |
20 | 95.2% | 87.1% | 50.0% | 19.7% | 18.4% | 17.0% | 15.7% | 14.4% | 13.0% | 11.7% | 7.5% |
30 | 95.2% | 87.7% | 80.3% | 50.0% | 25.1% | 23.1% | 21.0% | 19.0% | 17.0% | 15.0% | 10.5% |
40 | 95.2% | 88.3% | 81.6% | 74.9% | 50.0% | 29.1% | 26.4% | 23.6% | 20.9% | 18.2% | 13.5% |
50 | 95.2% | 89.0% | 83.0% | 77.0% | 70.9% | 50.0% | 31.7% | 28.3% | 24.9% | 21.5% | 16.5% |
60 | 95.2% | 89.6% | 84.3% | 79.0% | 73.6% | 68.3% | 50.0% | 32.9% | 28.8% | 24.8% | 19.6% |
70 | 95.2% | 90.3% | 85.6% | 81.0% | 76.3% | 71.7% | 67.1% | 50.0% | 32.8% | 28.0% | 22.6% |
80 | 95.2% | 90.9% | 87.0% | 83.0% | 79.1% | 75.1% | 71.2% | 67.2% | 50.0% | 31.3% | 25.6% |
90 | 95.2% | 91.5% | 88.3% | 85.0% | 81.8% | 78.5% | 75.2% | 72.0% | 68.7% | 50.0% | 28.6% |
100 | 99.1% | 95.5% | 92.5% | 89.5% | 86.5% | 83.5% | 80.4% | 77.4% | 74.4% | 71.4% | 50.0% |
Mythras
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 | 6.8% | 6.5% | 5.9% | 5.4% | 4.9% | 4.4% | 3.9% | 3.4% | 2.9% | 2.4% | 2.1% |
10 | 11.7% | 11.3% | 10.3% | 9.3% | 8.3% | 7.4% | 6.4% | 5.5% | 4.5% | 3.6% | 3.1% |
20 | 21.5% | 21.1% | 19.7% | 17.7% | 15.7% | 13.7% | 11.7% | 9.8% | 7.8% | 5.9% | 4.8% |
30 | 31.4% | 30.9% | 29.4% | 27.0% | 24.0% | 21.0% | 18.0% | 15.1% | 12.1% | 9.2% | 7.6% |
40 | 41.2% | 40.6% | 39.2% | 36.8% | 33.4% | 29.4% | 25.4% | 21.4% | 17.4% | 13.5% | 11.3% |
50 | 51.0% | 50.4% | 49.0% | 46.6% | 43.1% | 38.7% | 33.7% | 28.7% | 23.7% | 18.8% | 16.1% |
60 | 60.8% | 60.2% | 58.8% | 56.4% | 52.9% | 48.5% | 43.1% | 37.1% | 31.1% | 25.1% | 21.8% |
70 | 70.6% | 69.9% | 68.5% | 66.1% | 62.7% | 58.3% | 52.8% | 46.4% | 39.4% | 32.4% | 28.6% |
80 | 80.4% | 79.6% | 78.2% | 75.9% | 72.5% | 68.0% | 62.6% | 56.2% | 48.8% | 40.8% | 36.4% |
90 | 90.2% | 89.3% | 88.0% | 85.6% | 82.2% | 77.8% | 72.4% | 66.0% | 58.6% | 50.1% | 45.2% |
100 | 95.1% | 94.2% | 92.9% | 90.6% | 87.2% | 82.9% | 77.6% | 71.2% | 63.8% | 55.5% | 50.4% |
OpenQuest 3
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 | 50.0% | 49.4% | 47.1% | 44.1% | 40.2% | 35.5% | 30.0% | 23.8% | 16.6% | 8.7% | 0.0% |
10 | 50.4% | 50.0% | 47.7% | 44.6% | 40.8% | 36.1% | 30.6% | 24.3% | 17.2% | 9.3% | 0.5% |
20 | 52.7% | 52.2% | 50.0% | 47.0% | 43.1% | 38.4% | 32.9% | 26.6% | 19.5% | 11.6% | 2.8% |
30 | 55.7% | 55.3% | 53.1% | 50.1% | 46.2% | 41.5% | 36.0% | 29.7% | 22.6% | 14.6% | 5.9% |
40 | 59.6% | 59.2% | 57.0% | 54.0% | 50.2% | 45.5% | 40.0% | 33.7% | 26.5% | 18.6% | 9.8% |
50 | 64.3% | 63.8% | 61.7% | 58.7% | 54.9% | 50.3% | 44.8% | 38.4% | 31.3% | 23.3% | 14.5% |
60 | 69.8% | 69.3% | 67.2% | 64.2% | 60.4% | 55.8% | 50.3% | 44.0% | 36.8% | 28.9% | 20.1% |
70 | 76.1% | 75.7% | 73.5% | 70.5% | 66.7% | 62.1% | 56.7% | 50.4% | 43.2% | 35.2% | 26.4% |
80 | 83.2% | 82.8% | 80.6% | 77.6% | 73.8% | 69.2% | 63.8% | 57.5% | 50.4% | 42.4% | 33.6% |
90 | 91.2% | 90.7% | 88.5% | 85.6% | 81.8% | 77.1% | 71.7% | 65.5% | 58.4% | 50.5% | 41.6% |
100 | 100.0% | 99.6% | 97.4% | 94.4% | 90.6% | 86.0% | 80.5% | 74.3% | 67.2% | 59.3% | 50.5% |
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 <= pct) then
if p < math.ceil(p / 20) then
return STATUS_CRITICAL
elseif p < math.ceil(p / 5) then
return STATUS_SPECIAL
else
return STATUS_SUCCESS
end
else
local fumble = 100 - math.floor((100 - pct) / 20)
if p == 100 or 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 == 100 then
return STATUS_FUMBLE
elseif p <= pct then
if p == 1 then
return STATUS_CRITICAL
elseif p <= math.ceil(pct / 5) then
return STATUS_HARD
elseif p <= math.ceil(pct / 2) then
return STATUS_EXTREME
else
return STATUS_SUCCESS
end
else
if 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)
if p >= 96 then
-- 96 ... 100 is always a failure
if p == 100 or (p == 99 and pct <= 100) then
return STATUS_FUMBLE
end
return STATUS_FAILURE
end
if p <= pct or p <= 5 then
-- 1 ... 5 is always a success
if p <= math.ceil(pct / 10) then
return STATUS_CRITICAL
end
return STATUS_SUCCESS
else
return STATUS_FAILURE
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 > rstat
or (pstat == rstat and pstat >= STATUS_SUCCESS and p >= r)
end
-- Whether the roll is a critical success or critical failure (fumble)
-- in OpenQuest 3rd edition.
local function is_oq3_crit(r)
return r % 11 == 0 or r == 100
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 <= pct then
if is_oq3_crit(p) then
return STATUS_CRITICAL
else
return STATUS_SUCCESS
end
else
if is_oq3_crit(p) 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 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("%5.1f%% ", results[n] * 100))
end
table.insert(buf, "")
return table.concat(buf, "|")
end
local function print_table(min, max, step)
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},
}
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
print_table(0, 100, 10)
-
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. ↩︎