Examples
plotknot ships with example stories in the examples/ directory. Each demonstrates specific features. All examples are used in the test suite — they compile, load, and run through the runtime.
New to plotknot? Start with the Game Designer Tutorial (no code) or the Programmer Tutorial (variables, conditions, game interop) before diving into these examples.
hello.plotknot
Demonstrates: sections, choices, conditional choices, variables, call, if/else, goto.
A simple forest branching story — the “hello world” of plotknot:
# The Forest
set courage = 5
You stand at the edge of a dark forest. The path ahead splits in two.
choice:
- Take the left path -> left_path
- Take the right path -> right_path
- Turn back -> turn_back [if courage < 3]
## Left Path
You follow the mossy trail deeper into the woods.
call check_weather() -> weather
if weather == "rain":
Rain patters on the canopy above.
else:
Sunlight filters through the leaves.
-> ending
## Right Path
The right path leads to a sunlit clearing. A deer watches you pass.
-> ending
## Turn Back
Courage fails you today. The forest can wait.
-> ending
## Ending
Your journey through the forest ends here.
Key concepts:
setinitializes a variable- Conditional choice
[if courage < 3]only appears when the condition passes callinvokes a game function and stores the resultif/elseshows different text based on runtime state->navigates between sections
characters.plotknot
Demonstrates: character metadata, dialogue attribution.
A tavern scene with named speakers:
character: Mara
character: Aldric
character: Narrator
# The Tavern
Narrator: The tavern is warm and loud. Mara sits alone.
Mara: You look lost, traveler.
Aldric: Just passing through.
Mara: Nobody just passes through Thornwall.
choice:
- Ask about the road -> road_info
- Ask about the town -> town_info
Key concepts:
character:declarations register speakersName:prefix attributes dialogue lines- The runtime delivers
speakerfields for UI binding
variation.plotknot
Demonstrates: variation blocks, cycle and once modes.
A crossroads that changes description on revisit:
# The Crossroads
variation: cycle
You arrive at a weathered crossroads.
The crossroads again. The signpost's paint is peeling.
You know this crossroads by heart now.
choice:
- Take the forest path -> forest
- Take the river path -> river
- Rest here -> rest
## Rest
variation: once
You sit on the milestone and catch your breath.
-> the_crossroads
Key concepts:
variation: cyclerotates through text lines on each visitvariation: onceshows text only the first time- The runtime tracks visit counts per section
conditions.plotknot
Demonstrates: built-in functions, visited tracking, resource management.
A dungeon crawl with state-dependent content:
# Dungeon Entrance
set torches = 3
set depth = 0
The dungeon mouth yawns before you.
choice:
- Descend -> descend
- Turn back -> turn_back
## Descend
set depth = depth + 1
if torches > 0:
set torches = torches - 1
Your torch flickers, casting long shadows.
else:
Darkness presses close.
if visited("descend"):
You recognize this stretch of corridor.
else:
The corridor is new. Every shadow could hide danger.
choice:
- Go deeper -> descend
- Search for treasure -> treasure [if depth >= 3]
- Retreat to surface -> surface
Key concepts:
visited("section")checks if a stage was previously entered- Variables track resources (torches, depth)
- Conditional choices gate content behind progress
functions.plotknot
Demonstrates: custom functions, built-in math functions.
An alchemist shop with calculations:
function: calculate_dosage(2)
# Alchemist Shop
set gold = 50
set reputation = 10
set herb_count = 5
set potency = 3
call calculate_dosage(herb_count, potency) -> dose
if dose > 10:
"A potent mix," the alchemist mutters.
else:
"Mild stuff. Good for beginners."
choice:
- Buy healing potion (20 gold) -> buy_potion [if gold >= 20]
- Ask about rare ingredients -> rare_info [if reputation >= 15]
- Leave -> leave
## Buy Potion
set gold = gold - 20
set health = min(health + 30, 100)
The potion glows faintly blue.
-> alchemist_shop
Key concepts:
function:declares a custom function signaturecallinvokes it and stores the result- Built-in
min()clamps values - Choices gate on variable state
interpolation.plotknot
Demonstrates: text interpolation, inline tags, character properties, dismissible choices, nested choice content, stop.
A cafe scene with dynamic dialogue and styling cues:
declare gold = 50
declare visited_cafe = false
# The Cafe
set barista.friendship = 0
<ambient> The cafe hums with quiet conversation. Steam curls from the espresso machine.
cycle
You push through the door for the first time. A bell chimes.
--
The bell chimes again. The barista looks up.
--
You walk in like you own the place.
$barista polishes a glass behind the counter.
choice:
-- Ask about the special blend -> special
set barista.friendship = barista.friendship + 1
- Order a coffee -> order
- Leave -> leave
## Special
<friendly> "The special? You've got good taste," says $barista.
set gold = gold - 8
You sip something extraordinary. Worth every coin.
-> the_cafe
## Order
A plain coffee. Reliable. You have $gold gold left.
choice:
- Another round -> order [if gold >= 3]
set gold = gold - 3
- Leave -> leave
## Leave
if gold <= 0:
<tense> Your pockets are empty. Time to go.
stop
You step back into the street. Maybe tomorrow.
stop
Key concepts:
$varand${expr}insert live values into narrative text<tag>and<tag prop=val>attach styling metadata for the game UIcyclevariation changes the entrance text on each visit--dismissible choices disappear after being selected- Indented statements under choices run before navigation
stopends the story explicitlydeclareseeds initial variable values
variation_blocks.plotknot
Demonstrates: all five variation modes (sequence, cycle, once, pick, shuffle).
A guard post and market square showing each mode:
# Guard Post
sequence
The guard eyes you suspiciously. "First time in town?"
--
The guard nods. "Back again?"
--
The guard waves you through without a word.
-> market
## Market
shuffle
A merchant hawks fresh bread.
--
Children chase a stray dog between the stalls.
--
A musician plays a lilting tune by the fountain.
--
Two old women argue about the price of turnips.
choice:
- Browse the stalls -> browse
- Visit the fountain -> fountain
## Browse
pick
You find a dusty old map.
--
A vendor offers you a free sample of cheese.
--
Someone's dropped a shiny button.
-> market
## Fountain
once
The fountain burbles peacefully. You toss in a coin.
--
The fountain again. Your coin glints at the bottom.
cycle
Pigeons coo on the fountain's rim.
--
A child splashes in the shallows.
--
An old man fills his waterskin.
choice:
- Return to the market -> market
- Leave town -> leave
## Leave
You pass the guard one last time.
-> guard_post
Key concepts:
sequenceplays items in order and sticks on the lastshuffleplays all items in random order, then reshufflespickchooses a random item each time (items can repeat)onceplays items in order, then produces nothingcycleloops through items indefinitely- Items are separated by
--and can contain multiple lines
imports.plotknot
Demonstrates: multi-file imports, namespaced stage IDs.
A story split across files. The main file imports two modules:
import characters
import "scenes/tavern.plotknot"
# Village Square
declare reputation = 0
The village square bustles with morning activity.
choice:
- Visit the tavern -> scenes.tavern
- Talk to the elder -> characters.elder
- Leave the village -> leave
## Leave
You wave goodbye. Your reputation here is $reputation.
stop
The imported characters.plotknot file:
# Elder
The elder sits on a weathered bench, feeding pigeons.
set elder.wisdom = 10
if elder.wisdom > 5:
"Sit," the elder says. "Let me tell you about this place."
-> village_square
The imported scenes/tavern.plotknot file:
# Tavern
The tavern is warm and loud. A fire crackles in the hearth.
choice:
- Order a drink -> drink
- Return to the square -> village_square
## Drink
set reputation = reputation + 1
The ale is good. The company is better.
-> tavern
Key concepts:
import nameresolves toname.plotknotin the same directoryimport "path"uses a literal file path- Imported sections are namespaced:
characters.elder,scenes.tavern - Namespaced IDs are used in goto targets and choice destinations
- Variables and character properties are shared across all imported files
complete.plotknot
Demonstrates: all features combined in a substantial story.
A sci-fi narrative (“The Awakening”) with 15 sections exercising:
- Hub-and-spoke structure
- Multiple conditional choices
- Variable tracking (alert level, trust, power)
- Game function calls
- if/else branching
- Multiple endings based on accumulated state
This is the best reference for how all features compose in a real story. Read it in examples/complete.plotknot.
Running examples
Compile any example:
./plotknot compile examples/hello.plotknot --output /tmp/
Run through the Lua runtime:
local plotknot = require("plotknot")
local data = require("hello")
local story = plotknot.load(data)
story:start()
for _, line in ipairs(story:current()) do
if line.type == "text" then print(line.text) end
end
Real-world patterns
These patterns combine multiple features. Each is a skeleton you can adapt.
Branching dialogue with NPC memory
Track what the player has said to an NPC using character properties and conditions:
# Blacksmith
declare blacksmith.trust = 0
declare blacksmith.met = false
if not blacksmith.met:
A broad-shouldered woman hammers at the anvil.
She looks up. "First time here?"
set blacksmith.met = true
else:
cycle
The blacksmith nods. "Back again."
--
"More work for me?"
choice:
- Ask about the crypt -> crypt_talk [if not blacksmith.crypt_told]
- Ask about the crypt -> crypt_repeat [if blacksmith.crypt_told]
- Buy a sword (50 gold) -> buy_sword [if gold >= 50]
- Leave -> village_hub
## crypt_talk
"The crypt? Aye, I know it."
"My grandfather sealed the lower levels. Said something was down there."
set blacksmith.crypt_told = true
set blacksmith.trust = blacksmith.trust + 1
if blacksmith.trust >= 2:
"Tell you what. Take this key. It opens the lower gate."
set has_crypt_key = true
-> blacksmith
Features used: character properties (blacksmith.trust), declare, cycle variation, conditional choices, nested if.
Replayable encounters
Use visited() and variation blocks to make encounters feel different on replay:
# Ambush
set encounter_count = visited("ambush")
if encounter_count == 0:
Bandits leap from the trees! "Your gold or your life!"
else:
pick
The same bandits again. They look less confident this time.
--
"Not you again!" The bandits draw their weapons reluctantly.
--
The bandit leader sighs. "Just take the road. We're done."
choice:
- Fight -> combat
- Pay 20 gold -> pay [if gold >= 20]
- Intimidate -> intimidate [if strength > 12]
Features used: visited(), pick variation, conditional choices, if/else.
Quest state machine
Track quest progress with variables and call statements:
# Quest Board
choice:
- Accept the crypt job -> accept_crypt [if not quest.crypt_accepted]
- Check crypt progress -> crypt_status [if quest.crypt_accepted]
- Leave -> town_hub
## accept_crypt
"The crypt beneath the church. Something's stirring down there."
"Five hundred gold when the soulstone is secured."
set quest.crypt_accepted = true
set quest.crypt_stage = "accepted"
call notify_quest_log("crypt", "accepted")
-> town_hub
## crypt_status
if quest.crypt_stage == "accepted":
"The crypt awaits. The church is at the east end of town."
else:
if quest.crypt_stage == "soulstone_secured":
"You did it. Here's your gold."
set gold = gold + 500
set quest.crypt_stage = "complete"
call notify_quest_log("crypt", "complete")
-> town_hub
Features used: dot-notation variables (quest.crypt_stage), call for game interop, nested if/else, conditional choices.
Next steps
- Language Guide — every feature explained
- Best Practices — conventions for clean stories
- Extensibility — runtime hooks for game integration
All examples are verified in the test suite (tests/test_runtime.lua) — they compile, load, start, and produce expected output.