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
  • $torches inserts the current value of torches
  • ${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