Toward a Ritual Magic System, Part 3: Lore Redux and Ritual Procedure

Posted: 2024-02-28
Last Modified: 2024-10-20
Word Count: 2320
Tags: lua-code openquest openquest-content ritual-magic rpg writing-rpgs

Table of Contents

This work is based on the OpenQuest System Resource Document (found at https://openquestrpg.com/srd), a D101 Games product developed, authored by Newt Newport with Paul Mitchener. OpenQuest System Resource Document © 2021 by Newt Newport with Paul Mitchener is licensed under Attribution 4.0 International. To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/

The following text is © 2024 by Frank Mitchell. All rights reserved.

Lore Revisited

Last time I defined Lore thus:

Lore is a measure of how much the character understands the working of magic. It’s stated in the same terms as a system’s skills: percentiles in D100, modifiers to a d20 roll in d20, a number of dice in dice pool systems, etc.

After some thought I decided I’d rather have a system-independent definition of Lore to match the system-independent definition of Essence.

Lore is independent of what host system skills a magician uses to translate a ritual from Ancient Illyrian or whatever. It’s the ability to see beyond the details of the ritual to its fundamentals, and perform only what’s necessary.

Since I settled on d6es for Essence, I’ve decided the mechanic for deciphering and understanding a Ritual will be as follows:

  1. If the character has never performed a ritual before, their Lore always starts at 0. The lists of Known Rituals, Used Rituals, and Unknown Rituals start off empty.
  2. The character has a starting dice pool equal to their Lore.
    • Subtract two dice if they are inventing a ritual from scratch.
    • Subtract one die if they are attempting to modify a ritual.
    • Subtract one die if the ritual was translated from a language the ritualist does not know well.
    • Subtract one die if the ritual isn’t part of the character’s Tradition, as noted by the spell or decided by the GM.
    • Add one die if they have deciphered this ritual before.
    • Add two dice if they have successfully performed this ritual before.
  3. If the dice pool is 0 the player rolls two dice; the result is the lower die. Otherwise the player rolls the dice pool; the result is the highest die.
  4. The result of the Lore check must be equal or greater to the Ritual Difficulty, a number from 1 (Trivial) to 6 (Very Difficult).
    • If the Ritual Difficulty is 1, no roll need be made; it succeeds automatically.
  5. If the Lore Check succeeds, the character may add the Ritual to their list of Known Rituals. If it fails, the character adds that ritual to their list of Unknown Rituals. They may not attempt to decipher that particular ritual again until their Lore increases.
  6. At the end of each session in which the Ritualist performs a Lore Check, whether it succeeds or fails, the player may rolls their most difficult Lore Check again. If the Check fails, the character’s Lore increases by one. They also remove all Rituals from the set of Unknown Rituals.
    • From this it follows that Lore increases more quickly from trying to decipher difficult rituals, as often as possible.

Optional Rule: Recording and Memorizing Rituals

Under the rules above, once a character deciphers a ritual, they know it forever. Here we consider whether the ritual is recorded and/or memorized.

Performing a Ritual

The procedure for handling Ritual Magic in a game using these rules is this:

  1. The Ritual Leader assembles any required Assistants, implements, and Offerings at an appointed place and time.
    • Assistants are co-celebrants who lend their Essence to the Ritual.
    • Implements are tools or components that are not consumed during the Ritual.
    • Offerings are components that are consumed, or at least transformed, during the ritual. They may be raw materials, finished goods, animal sacrifices, or in the darkest rituals human sacrifices.
  2. The Leader begins the ritual, with Assistants joining in as required.
  3. If a Offering is required, the Offering is made at the end of the Ritual.
  4. The Leader’s player collects a pool of d6s by adding the following:
    • The Leader’s Essence
    • The Assistants’ Essence
    • Essence derived from the Time and Place.
    • Essence derived from any Relics present.
    • Essence derived from the Offering(s).
  5. The Essence Pool loses one die for every repeated attempt at a ritual until all Ritualists get a full night’s sleep. Certain other influence, such as ritual contamination, may subtract from the die pool.
  6. The player rolls all Essence Dice and compares the total to the ritual’s Essence Threshold(s). The total must equal or exceed the (highest) threshold in order to get the corresponding effect. If the total falls under the (lowest) Threshold, the ritual failed.
    • If the player does not want to roll a large number of dice, they may subtract pairs of dice and add 7 to the Essence Total for each pair. The player must still roll at least five dice unless the Total already exceeds the threshold for the desired effect.
  7. The ritual has the effects determined by the Essence Total.

Next Time

In the next segment we will drill down into the game description of a ritual, and some sample rituals that nearly every ritual magician would know.

Appendix A: Lore Check vs. Ritual Difficulty

Below are the probabilities of beating each Ritual Difficulty by number of Lore dice.

Dice 2 3 4 5 6
0d 69.44% 44.44% 25.00% 11.11% 2.78%
1d 83.33% 66.67% 50.00% 33.33% 16.67%
2d 97.22% 88.89% 75.00% 55.56% 30.56%
3d 99.54% 96.30% 87.50% 70.37% 42.13%
4d 99.92% 98.77% 93.75% 80.25% 51.77%
5d 99.99% 99.59% 96.88% 86.83% 59.81%
6d 100.00% 99.86% 98.44% 91.22% 66.51%
7d 100.00% 99.95% 99.22% 94.15% 72.09%
8d 100.00% 99.98% 99.61% 96.10% 76.74%
9d 100.00% 99.99% 99.80% 97.40% 80.62%
10d 100.00% 100.00% 99.90% 98.27% 83.85%
11d 100.00% 100.00% 99.95% 98.84% 86.54%
12d 100.00% 100.00% 99.98% 99.23% 88.78%

Difficulty 1 rituals always have a 100% chance of being deciphered.

Appendix B: Essence Check vs. Essence Threshold

Below are the probabilities of succeeding at a specific Threshold (TN) with a number of Essence Dice. This will prove useful when we begin defining Ritual Thresholds next time.

Note that instead of using Thresholds that are multiples of 3.5 rounded up, I decided to use a straight 3.

TN 1D 2D 3D 4D 5D 6D 7D 8D
3 66.67% 97.22% 100.00% 100.00% 100.00% 100.00% 100.00% 100.00%
6 16.67% 72.22% 95.37% 99.61% 99.99% 100.00% 100.00% 100.00%
9 27.78% 74.07% 94.60% 99.28% 99.94% 100.00% 100.00%
12 2.78% 37.50% 76.08% 94.12% 99.01% 99.88% 99.99%
15 9.26% 44.37% 77.85% 93.92% 98.79% 99.82%
18 0.46% 15.90% 50.00% 79.42% 93.88% 98.63%
21 2.70% 22.15% 54.64% 80.83% 93.93%
24 0.08% 5.88% 27.94% 58.58% 82.11%
27 0.72% 9.65% 33.22% 61.98%
30 0.01% 1.97% 13.72% 38.02%
33 0.18% 3.79% 17.89%
36 0.00% 0.61% 6.07%
39 0.04% 1.37%
42 0.00% 0.18%
TN 9D 10D 11D 12D 13D 14D 15D 16D
15 99.98% 100.00% 100.00% 100.00% 100.00% 100.00% 100.00% 100.00%
18 99.76% 99.97% 100.00% 100.00% 100.00% 100.00% 100.00% 100.00%
21 98.51% 99.71% 99.95% 99.99% 100.00% 100.00% 100.00% 100.00%
24 94.04% 98.43% 99.66% 99.94% 99.99% 100.00% 100.00% 100.00%
27 83.28% 94.20% 98.37% 99.63% 99.93% 99.99% 100.00% 100.00%
30 64.96% 84.35% 94.38% 98.34% 99.59% 99.92% 99.99% 100.00%
33 42.39% 67.60% 85.33% 94.57% 98.33% 99.57% 99.90% 99.98%
36 22.04% 46.37% 69.96% 86.24% 94.77% 98.33% 99.55% 99.89%
39 8.71% 26.11% 50.00% 72.08% 87.07% 94.98% 98.34% 99.53%
42 2.47% 11.60% 30.04% 53.33% 74.00% 87.85% 95.19% 98.37%
45 0.46% 3.90% 14.67% 33.81% 56.38% 75.75% 88.57% 95.39%
48 0.05% 0.94% 5.62% 17.83% 37.40% 59.19% 77.34% 89.24%
51 0.00% 0.15% 1.63% 7.60% 21.04% 40.81% 61.77% 78.80%
54 0.00% 0.01% 0.34% 2.54% 9.79% 24.25% 44.05% 64.16%
57 0.00% 0.05% 0.64% 3.67% 12.15% 27.44% 47.11%
60 0.00% 0.00% 0.11% 1.08% 5.02% 14.64% 30.57%
63 0.00% 0.01% 0.24% 1.67% 6.56% 17.22%
66 0.00% 0.00% 0.04% 0.43% 2.42% 8.27%
69 0.00% 0.00% 0.08% 0.72% 3.34%
72 0.00% 0.00% 0.01% 0.17% 1.11%
75 0.00% 0.00% 0.03% 0.29%
78 0.00% 0.00% 0.00% 0.06%
TN 17D 18D 19D 20D 21D 22D 23D 24D
36 99.98% 100.00% 100.00% 100.00% 100.00% 100.00% 100.00% 100.00%
39 99.88% 99.98% 100.00% 100.00% 100.00% 100.00% 100.00% 100.00%
42 99.52% 99.88% 99.97% 99.99% 100.00% 100.00% 100.00% 100.00%
45 98.39% 99.51% 99.87% 99.97% 99.99% 100.00% 100.00% 100.00%
48 95.59% 98.43% 99.51% 99.86% 99.97% 99.99% 100.00% 100.00%
51 89.86% 95.79% 98.47% 99.51% 99.86% 99.96% 99.99% 100.00%
54 80.14% 90.44% 95.98% 98.51% 99.51% 99.86% 99.96% 99.99%
57 66.37% 81.38% 90.98% 96.16% 98.55% 99.51% 99.85% 99.96%
60 50.00% 68.42% 82.52% 91.49% 96.34% 98.59% 99.52% 99.85%
63 33.63% 52.73% 70.32% 83.58% 91.97% 96.51% 98.64% 99.52%
66 19.86% 36.60% 55.30% 72.09% 84.56% 92.41% 96.67% 98.69%
69 10.14% 22.54% 39.49% 57.73% 73.73% 85.47% 92.83% 96.82%
72 4.41% 12.13% 25.23% 42.27% 60.02% 75.26% 86.32% 93.22%
75 1.61% 5.64% 14.23% 27.91% 44.95% 62.18% 76.69% 87.11%
78 0.48% 2.23% 7.01% 16.42% 30.58% 47.53% 64.21% 78.03%
81 0.12% 0.74% 2.97% 8.51% 18.67% 33.21% 50.00% 66.13%
84 0.02% 0.20% 1.07% 3.84% 10.13% 20.97% 35.79% 52.37%
87 0.00% 0.04% 0.32% 1.49% 4.83% 11.86% 23.31% 38.32%
90 0.00% 0.01% 0.08% 0.49% 2.01% 5.95% 13.68% 25.66%
93 0.00% 0.00% 0.02% 0.14% 0.72% 2.62% 7.17% 15.57%
96 0.00% 0.00% 0.00% 0.03% 0.22% 1.00% 3.33% 8.50%
99 0.00% 0.00% 0.00% 0.01% 0.06% 0.33% 1.36% 4.14%
102 0.00% 0.00% 0.00% 0.00% 0.01% 0.09% 0.48% 1.79%
105 0.00% 0.00% 0.00% 0.00% 0.02% 0.15% 0.68%
108 0.00% 0.00% 0.00% 0.00% 0.00% 0.04% 0.22%
111 0.00% 0.00% 0.00% 0.00% 0.01% 0.06%
114 0.00% 0.00% 0.00% 0.00% 0.00% 0.02%

Appendix C: Program for Lore Checks

Generator for this table.

#!/usr/bin/env lua

local NSIDES = 6

-- Chance of rolling `t` or better on one die 
local function success_1(t)
    if t <= 1 then
        return 1.0
    elseif t > NSIDES then
        return 0.0
    else
        return (NSIDES + 1 - t) / NSIDES
    end
end

-- Chances of rolling `t` or better on `n` 6-sided dice.
local function success(n, t)
    local p = success_1(t)
    if n < 1 then
        -- Special rule for dice pool <= 0:
        -- roll 2 dice, get `t` or higher on BOTH
        return p * p
    else
        return 1 - (1 - p)^n
    end
end

local function format_col(t)
    return string.format(" %-7d ", t)
end

local function format_percent(p)
    if p == 0 then
        return string.rep(" ", 9)
    end
    return string.format(" %6.2f%% ", p * 100)
end

local function print_header()
    local rowbuf

    rowbuf = { "Dice " }

    for t = 2,NSIDES do
        table.insert(rowbuf, format_col(t))
    end

    table.insert(rowbuf, "")
    print(table.concat(rowbuf, '|'))

    rowbuf = { ":---:" }
    for t = 2,NSIDES do
        table.insert(rowbuf, "--------:")
    end

    table.insert(rowbuf, "")
    print(table.concat(rowbuf, '|'))
end

local function print_row(n)
    local rowbuf = { string.format(" %2dd ", n) }

    for t = 2,NSIDES do
        table.insert(rowbuf, format_percent(success(n,t)))
    end

    table.insert(rowbuf, "")
    print(table.concat(rowbuf, '|'))
end

local function print_table(maxdice)
    local md = (maxdice or 10)

    print_header()

    for n = 0, md do
       print_row(n)
    end
end

print_table(12)

Appendix D: Program for Essence Checks

Generator for this table.

Modified from an earlier version.

#!/usr/bin/env lua

local NSIDES  <const> = 6
local MAXDICE <const> = 24
local PAGESIZ <const> = 8

local CACHE = {}

--[[
-- Chance that a roll of `ndice`D`nsides` == `target`
]]--
local function p_roll_eq(ndice, nsides, target)
    if ndice == 1 then
        if target >= 1 and target <= nsides then
            return (1/nsides)
        end
    elseif target >= ndice and target <= ndice * nsides then
        local key = string.format("%02d\t%02d\t%02d", ndice, nsides, target)
        local result = CACHE[key]
        if result then
            return result
        end

        -- sum(1, nsides, p(one die is i) * p(remaining dice == target - i))
        local sum = 0.0
        for i = 1, nsides do
            sum = sum + p_roll_eq(1, nsides, i) 
                            * p_roll_eq(ndice - 1, nsides, target - i)
        end
        CACHE[key] = sum
        return sum
    end
    return 0.0
end

--[[
-- Chance that a roll of `ndice` D `nsides` >= `target`
]]--
local function p_roll_ge(ndice, nsides, target)
    local sum = 0.0
    for t = target, nsides * ndice do
        sum = sum + p_roll_eq(ndice, nsides, t)
    end
    return sum
end

local function format_header(mindice, maxdice)
    local buf = { "", "  TN " }
    for n = mindice, maxdice do
        table.insert(buf, string.format(" %6dD ", n))
    end
    table.insert(buf, "")
    return table.concat(buf, "|")
end

local function format_bar(mindice, maxdice)
    local buf = { "", ":---:" }
    for n = 1, maxdice - mindice + 1 do
        table.insert(buf, "--------:")
    end
    table.insert(buf, "")
    return table.concat(buf, "|")
end

local function format_row(target, results)
    local buf = { "", 
        string.format(" %3d ", target),
    }
    for n = 1, #results do
        if results[n] == 0.0 then
            table.insert(buf, string.rep(' ', 9))
        else
            table.insert(buf, string.format(" %6.2f%% ", results[n] * 100))
        end
    end
    table.insert(buf, "")
    return table.concat(buf, "|")
end

local function feq(f1, f2)
    return math.abs(f1 - f2) < 0.0001
end

local function print_table(nsides, mindice, maxdice)
    print(format_header(mindice, maxdice))
    print(format_bar(mindice, maxdice))

    local step = nsides // 2
    local max = maxdice * nsides
    local min = step

    for t = min, max, step do
        -- Avoid rows with all 100% or all 0%
        if not (feq(p_roll_ge(mindice, nsides, t), 1.0)
                or feq(p_roll_ge(maxdice, nsides, t), 0.0)) then
            local result = {}
            for d = mindice, maxdice do
                local i = 1 + d - mindice
                result[i] = p_roll_ge(d, nsides, t)
            end
            print(format_row(t, result))
        end
    end
end

local function min(a, b)
    if a < b then
        return a
    else
        return b
    end
end

local function print_pages(nsides, maxdice, pagesiz)
    for p = 1, maxdice, pagesiz do
        print()
        print_table(nsides, p, min(maxdice, p + pagesiz - 1))
    end
end

print_pages(NSIDES, MAXDICE, PAGESIZ)