I’m currently running a D&D campaign, and in a recent session we ended with the plan for the party to interrogate a character they’d captured. We play every other week, which meant I had two whole weeks to figure out what exactly this character knew. I was worried about a few things though.
An interrogation where just one player with the highest Charisma asks all the questions, and the character resists (“No, no! I won’t tell you anything!”) so the player just keeps re-asking until they roll a high enough Intimidation check (“Ok! Fine! I give up! I’ll tell you everything!”) could be kind of boring. And I didn’t want the players to get bored and give up on the interrogation, because this character they captured actually had some pretty important information that could animate the party’s next steps. So I tried thought of ways I could structure the interrogation to have mechanics of its own, which led me to making a game board in Typst.
Typst
Typst is a new typesetting system that, imo, is way easier to use and customize than LaTeX (and there’s built in support in Quarto). There’s also a growing ecosystem of packages, including CeTZ (meant to the the Typst version of TiKZ) and fletcher which is built on top of CeTZ.
Embedding Typst Diagrams
While Quarto supports rendering entire documents to Typst, it doesn’t have the ability to run a Typst code chunk and embed the output in a different document format, like it can do with plotting R code chunks. For a separate project, I solved this by setting up a Makefile, and added running it to my pre-render script.
UNAME := $(shell uname)
ifeq ($(UNAME),Darwin)
TYPST=~/.local/bin/typst
else
TYPST=/usr/local/bin/typst
endif
srces := $(wildcard posts/20*/*/20*/typst/*.typ)
targets := $(patsubst %.typ,%.svg,$(srces))
all: $(srces) $(targets)
%.svg: %.typ
$(TYPST) compile --font-path fonts -f svg $< $@
Now, every time I re-render the post, changes I make to the typst source files will be reflected in the embedded images.
First fletcher diagram
Here’s an annotated version of the typst file to generate an A->B->C->A diagram in typst using fletcher.
// package imports
#import "@preview/fletcher:0.5.8" as fletcher: (
diagram,
node,
edge
)
// By default, the page size is A4.
// But since I'm making a standalone diagram,
// I want it to shrink automatically shrink
// to the size of the content.
#set page(
: auto,
width: auto,
height: 5mm,
margin: white
fill)
// This is a simple array of
// node labels
#let nodes = ("A", "B", "C")
// This is an array of from-to edges
#let edges = (
(0,1),
(1,2),
(2,0)
)
#diagram({
for (i, n) in nodes.enumerate() {
(
node// Node coordinates
(i, 0),
// node text
,
n// node line
: 1pt,
stroke// node id
: str(i)
name)
}
// argument unpacking
for (from, to) in edges {
// typed numeric variables
let bend = 0deg
// on the return arrow
// arc large
if(to < from){
= 60deg
bend }
(
edge(str(from)),
label(str(to)),
label// edge type
"-|>",
: bend
bend)
}
})
// package imports
#import "@preview/fletcher:0.5.8" as fletcher: (
diagram,
node,
edge
)
// By default, the page size is A4.
// But since I'm making a standalone diagram,
// I want it to shrink automatically shrink
// to the size of the content.
#set page(
: auto,
width: auto,
height: 5mm,
margin: rgb("#222")
fill)
#set text(
: white
fill)
// This is a simple array of
// node labels
#let nodes = ("A", "B", "C")
// This is an array of from-to edges
#let edges = (
(0,1),
(1,2),
(2,0)
)
#diagram({
for (i, n) in nodes.enumerate() {
(
node// Node coordinates
(i, 0),
// node text
,
n// node line
: 1pt+white,
stroke// node id
: str(i)
name)
}
// argument unpacking
for (from, to) in edges {
// typed numeric variables
let bend = 0deg
// on the return arrow
// arc large
if(to < from){
= 60deg
bend }
(
edge(str(from)),
label(str(to)),
label// edge type
"-|>",
: white,
stroke: bend
bend)
}
})
(I’ll put the rest of the typst code inside collapsed callouts).
The game board
A cool thing I hadn’t realized about the node placement in fletcher is you an either give it Cartesian coordinates like (1, 0)
, or you can give it polar coordinates, like (90deg, 10mm)
. The fletcher example diagram I was inspired by automatically chunks up the unit circle based in how many nodes there are.
So I popped the five Wh-words in English into the nodes list and got this:
Then, inspiration struck: Connecting 5 Wh-words with paths gives us the opportunity make the shape of the game board a pentagram. To make it more gamey and something the players would need to strategize, I made the paths out of each Wh-node directional, so you couldn’t get to every node from every node.
With a little more color and font flourishes, I got this final product:
#import "@preview/fletcher:0.5.8" as fletcher: (
diagram,
node,
edge
)#set page(
: auto,
width: auto,
height: 5mm,
margin: rgb("#393d3b")
fill)
#set text(
: "Macondo Swash Caps",
font: 30pt
size)
#set align(center)
#let nodes = ("Who", "What", "When", "Where", "Why")
#let edges = (
(3,0),
(0,2),
(2,4),
(4,1),
(1,3),
(0,1),
(1,2),
(2,3),
(3,4),
(4,0)
)
#box[
#set text(size: 0.8em, fill: gradient.linear(
.hsv(50deg, 50%, 70%),
color//color.hsv(50deg, 50%, 70%),
.hsv(0deg, 100%, 100%, 100%),
color.hsv(0deg, 100%, 100%, 100%),
color,
black: 90deg
angle),)
The interrogation of Bartholemew Durkis]
#diagram({
for (i, n) in nodes.enumerate() {
let θ = 90deg - i*360deg/nodes.len()
((θ, 50mm),
node,
n: 0.5pt,
stroke: str(i),
name: circle,
shape:gradient.linear(
fill(0%, 80%),
luma.hsv(0deg, 100%, 100%, 70%),
color.hsv(0deg, 100%, 100%, 50%),
color: -θ)
angle
)
}for (from, to) in edges {
(
edge(str(from)),
label(str(to)),
label"-|>",
: color.hsv(50deg, 50%, 70%) + 3pt,
stroke: 10deg,
bend)
}
(
node(0deg,0mm),
"Y/N",
:"6",
name:circle,
shape:0.5pt,
stroke: color.hsv(0deg, 100%, 100%, 60%)
fill)
for(i,m) in nodes.enumerate(){
(
edge("6"),
label(str(i)),
label"-}>",
: color.hsv(50deg, 50%, 70%) + 1.5pt,
stroke:10deg
bend)
}
) }
Combined with an animation I made separately and some mood music, I think the overall effect was good!
Reuse
Citation
@online{fruehwald2025,
author = {Fruehwald, Josef},
title = {D\&D and {Making} {Typst} {Figures}},
series = {Væl Space},
date = {2025-09-13},
url = {https://jofrhwld.github.io/blog/posts/2025/09/2025-09-13_typst-figures/},
doi = {10.59350/ewqr3-7w333},
langid = {en}
}