Idraluna Archives

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:

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!

#DIY #Typst