Randomization in Typst
The following is a very, very basic introduction to incorporating randomization into a Typst document.
Basic random number generation
Typst is a functional language -- I am not remotely qualified to unpack the nuances here, but for our purposes it means that it is designed to be deterministic: if you feed it a given input, you will always get the same compiled pdf. Thus, Typst only has pseudo-random generators, where each random input 'seed' yields a determined output but the relationship between input seed & output value is effectively random.
There is a simple plugin for Typst called dice, but it's not well-documented so I prefer using the suji package.
Here's a simple function to roll an arbitrary number of dice & take their sum:
#import "@preview/suiji:0.4.0": *
#let rXdY(seed, X, Y) = {
let rng = gen-rng-f(seed)
let arr = ()
(rng, arr) = integers-f(rng, low: 1, high: Y + 1, size: X)
return arr.sum()
}
The output from rolling 3d6 for each random seed from 1-20 is: 12 12 14 6 8 11 14 13 12 9 10 7 15 7 14 12 10 12 7 12
Note that the first three lines in the function generate an array of die results, & the last line takes their sum. We can add the option to return the highest or lowest result:
#let rXdYadvanced(seed, X, Y, fun: "sum") = {
let rng = gen-rng-f(seed)
let arr = ()
(rng, arr) = integers-f(rng, low: 1, high: Y + 1, size: X)
if fun == "sum" {return arr.sum()} else if fun == "max" {return calc.max(..arr)} else {return calc.min(..arr)}
}
Note that the calc.min() function requires that we use the spread operator (..) to change the list of die results in the array named arr into a series of sequential inputs to the function (otherwise, it treats the array of both die rolls as a single input with nothing to compare it to).
Generating pseudo-random seeds
The easiest way to generate a random seed is to derive it from the current date:
#let date_seed = datetime.today().year() + datetime.today().month() + datetime.today().day()
But keep in mind that if you just plug this into every die roll, you'll get the same result throughout your document, so each use of a random function will need to modify the seed by some amount.
I thus prefer to tie random generation to some unique aspect of the thing being generated. For my NPC supplement, I came up with a very silly "nominative determinism" approach that extracts a seed from an NPC's name:
#let charmap = ("a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, "h": 8, "i": 9, "j": 10, "k": 11, "l": 12, "m": 13, "n": 14, "o": 15, "p": 16, "q": 17, "r": 18, "s": 19, "t": 20, "u": 21, "v": 22, "w": 23, "x": 24, "y": 25, "z": 26, " ": 100, ".": 1000, "-": 417, "1": -1, "2": -2, "3": -3, "4": -4)
#let name_to_number(string) = {
let tot = 0
let i = 0
for c in lower(string) {
if not c in charmap {
// ignore the character
} else {
i += 1
tot += charmap.at(c) * i
}
}
return tot
}
This doesn't have to rely on an NPC name -- you could do something similar for hex coordinates, or dungeon room & level, Traveller planet profile, etc.
Rolling on a table
To randomly choose an entry from a table, plug in the table as an array, and then feed a random die roll into the .at() method.
#let example_table = ("Fighter", "Cleric", "Mage", "Thief")
#let random_table_pick(seed, table) = {
let rng = rXdY(seed, 1, table.len())
return table.at(rng - 1)
}
Statblock generator example
Putting it together, here's an example of a dirt-simple random NPC statblock generator. First, we generate the stats and store them in a Typst dictionary, which lets us reference stats by name.
#let random_npc(charname) = {
let seed = name_to_number(charname)
let classlist = ("Fighter", "Cleric", "Mage", "Thief")
return (
Name: charname,
Class: random_table_pick(seed, classlist),
STR: rXdY(seed+1, 3, 6),
INT: rXdY(seed+2, 3, 6),
WIS: rXdY(seed+3, 3, 6),
CON: rXdY(seed+4, 3, 6),
DEX: rXdY(seed+5, 3, 6),
CHA: rXdY(seed+6, 3, 6),
)
}
Next, we can write a function to render the stats as a small table inside a box, with the dictionary as input:
#let render_npc(statblock) = box(stroke: 1pt, inset: 4pt)[
*Name:* #statblock.Name \
*Class:* #statblock.Class
#table(
columns: 6,
stroke: none,
[STR], [INT], [WIS], [CON], [DEX], [CHA],
[#statblock.STR], [#statblock.INT], [#statblock.WIS], [#statblock.CON], [#statblock.DEX], [#statblock.CHA]
)
]
Plugging in #render_npc(random_npc("Simon Sneed")) gives us:
Notes & use cases
Randomization in Typst is a bit kludge-y (though I'm sure there are more elegant ways to do it). In most cases, it's easier to do your random generation outside Typst & add the results to your document. But I have a few ideas for how it might be handy:
- You could set up a document that takes a single input seed & then iteratively generate a bunch of variations for different seeds. This could be useful for a printable dungeon or hexcrawl template that has a randomized prompt for each hex/room. (Could be useful for a jam, or to hand out at a con).
- You could use it to automate trivial features of a larger scenario, stuff where you want the information to be printed but don't need to make it unique or interesting. For example, castles in OD&D usually have 1-6 lower-level retainers serving a higher-level NPC. If you're writing a scenario with lots of castles, you could use Typst to automatically spit out statblocks for these minor NPCs.
- It's good for quickly generating long lists of pre-rolled randomness. In the NPC supplement mentioned above, I used it to pre-roll stats for 200 NPC hirelings. You could probably hack together a neat document with pre-rolled treasure caches (organized by treasure type or dungeon level) or NPC names, etc. (Whether this is more efficient than copy-pasting something generated by Perchance is debatable, though).
In any case, I hope this proves useful for someone, somehwere. If you've come up with a clever use for random generation in Typst, I'd love to hear about it!