Tutorial: Scripting for Programmers
This tutorial is for programmers integrating plotknot into a game. You’ll build a small dungeon scene that uses variables, conditions, built-in functions, game interop, and text interpolation. By the end, you’ll understand how plotknot scripts talk to your game code.
Prerequisites
- plotknot compiler built and available (
plotknot --version) - Basic understanding of variables and conditionals (any language)
- No Lua or game engine knowledge required for this tutorial
What you’ll build
A dungeon crawl scene: the player explores rooms, rolls dice for traps, tracks health and torches, and calls game functions for combat resolution.
Step 1: Declare starting variables
Use declare to set initial values before any stage runs. Declarations are collected by the compiler and emitted as the starting state:
# The Tower
declare health = 100
declare torches = 3
declare depth = 0
declare gold = 0
declare is for variables that need an initial value before the story starts. Use set for assignments that happen during play.
Step 2: Use variables in text
Embed variable values directly in narrative text with $:
# The Tower
declare health = 100
declare torches = 3
declare depth = 0
<tense> The tower looms above you. Its entrance gapes like a hungry mouth.
You have $torches torches and $health health.
choice:
- Enter the tower -> enter
- Walk away -> leave
$torchesinserts the current value oftorches${expr}inserts the result of any expression:${torches - 1}$$produces a literal dollar sign
The runtime assembles interpolated text at display time, so values always reflect current state.
Step 3: Modify variables with set
## Enter
set depth = depth + 1
if torches > 0:
set torches = torches - 1
You light a torch and descend. You have $torches left.
else:
<dark> Darkness swallows you whole.
-> ending
Supported operators: +, -, *, /, ==, !=, <, >, <=, >=, and, or, not.
Step 4: Branch with conditions
if/else blocks show content dynamically:
set roll = dice(20)
if roll <= 5:
A trap springs! You lose ${10 + depth} health.
set health = health - (10 + depth)
else:
The passage is safe. For now.
if health <= 0:
Your vision fades. The tower claims another soul.
stop
The stop keyword ends the story. Your game code can check story:stopped() to show a game-over screen.
Step 5: Use built-in functions
plotknot provides built-in functions for common game logic:
## Treasure Room
set loot = random_range(10, 50)
set gold = gold + loot
You find a chest containing $loot gold!
set roll = dice(20)
if chance(4):
A critical find! You discover a hidden compartment.
set gold = gold + dice(10)
| Function | What it does |
|---|---|
dice(sides) |
Random integer from 1 to sides |
random() |
Random float from 0 to 1 |
random_range(a, b) |
Random integer from a to b inclusive |
chance(n) |
Returns true with 1-in-n probability |
min(a, b) / max(a, b) |
Smaller / larger value |
round(n) / floor(n) / ceil(n) |
Rounding |
visited(section) |
Visit count for a stage |
seen(section) |
true if visited at least once |
Step 6: Gate choices with conditions
Add [if expr] to any choice to show it only when the condition passes:
choice:
- Go deeper -> enter
- Search for treasure -> treasure [if depth >= 3]
- Retreat to the surface -> ending
The runtime evaluates conditions and only includes passing choices. Choices without conditions always appear.
Step 7: Call game functions
The call statement invokes a function registered in your game code:
## Battle
call calculate_damage(attack, defense) -> damage
set health = health - damage
if health <= 0:
-> game_over
else:
-> continue_fight
The -> result syntax stores the return value in a variable. Without it, the call is fire-and-forget:
call play_sound("explosion")
call shake_camera(3)
Your game code registers handlers for these calls. See the Runtime API for the Lua side.
Step 8: Declare external functions
Tell the compiler about functions your game code provides:
# NPC Interactions
function npc_mood(1)
function calculate_damage(2)
if npc_mood("blacksmith") == "friendly":
"Good to see you again!"
set damage = calculate_damage(attack, defense)
The syntax is function name(param_count). This:
- Suppresses “unknown function” warnings
- Validates argument count at compile time
- Enables LSP autocompletion
The runtime provides the actual implementation via story:register_function(name, fn).
Step 9: Use character properties
Track NPC state separately from global variables with dot notation:
## Keeper
set keeper.friendship = keeper.friendship + 1
if keeper.friendship > 1:
<friendly> $keeper smiles. "Back again? Take this."
set torches = torches + 1
else:
The keeper squints at you. "Torches cost extra."
Character properties live in a separate chars table in the runtime. $keeper in text inserts the character’s name property.
Step 10: Add inline tags
Attach styling metadata for your game’s UI:
<tense> The tower looms above you.
<friendly> "Welcome back, friend!"
<shake intensity=3> The ground trembles.
Tags are delivered to the runtime as metadata alongside the text. Your game code reads them to drive animations, audio, or visual effects.
The complete scene
declare health = 100
declare torches = 3
declare depth = 0
declare gold = 0
function calculate_damage(2)
# The Tower
set keeper.friendship = 0
<tense> The tower looms above you. Its entrance gapes like a hungry mouth.
cycle
You approach for the first time. The stones hum with old magic.
--
The tower again. The hum is almost familiar now.
--
You know every crack in these walls.
choice:
- Enter the tower -> enter
set route = "brave"
- Ask the keeper for advice -> keeper
- Walk away -> leave
## Keeper
set keeper.friendship = keeper.friendship + 1
if keeper.friendship > 1:
<friendly> $keeper smiles. "Back again? Take this."
set torches = torches + 1
else:
The keeper squints at you. "Torches cost extra."
choice:
-- Take the free torch -> enter [if keeper.friendship > 1]
- Enter the tower -> enter
- Walk away -> leave
## Enter
set depth = depth + 1
if torches > 0:
set torches = torches - 1
You light a torch and descend. You have $torches left.
else:
<dark> Darkness swallows you whole.
-> ending
set roll = dice(20)
if roll <= 5:
A trap springs! You lose ${10 + depth} health.
set health = health - (10 + depth)
if health <= 0:
Your vision fades. The tower claims another soul.
stop
choice:
- Go deeper -> enter
- Search for treasure -> treasure [if depth >= 3]
- Retreat to the surface -> ending
## Treasure
set loot = random_range(10, 50)
set gold = gold + loot
You find a chest containing $loot gold!
-> enter
## Leave
You turn your back on the tower. Some mysteries are best left alone.
-> ending
## Ending
Your adventure ends here. You reached depth $depth.
stop
Compile and verify
plotknot compile tower.plotknot --output output/
plotknot validate tower.plotknot
What’s next
- Defold Integration Tutorial — run this story in a Defold project
- Runtime API — the Lua runtime, line types, and state queries
- Extensibility — on_call, register_function, on_line_type, on_stage
- Language Reference — formal grammar and all keywords