Scripting

A script is a list of instructions Merlin runs in order. Each line starts with a verb that names the action. This guide covers every verb, wait type, and control structure in the scripting language, along with the rules that make each one predictable. The language is the same in every game. The examples use game variables such as $health and $target, which come with your game's module. To see the ones your game has, go to Settings > Variables.

Write and run a script

  1. In the Scripts panel, create a script named hello.
  2. Enter these lines:
    echo I am about to wave.
    send wave
    echo I waved!
  3. Save the script.
  4. To run it, type .hello in the command line.

You see:

Merlin Client: I am about to wave.
> wave
You wave.
Merlin Client: I waved!

echo prints a message only you see. Merlin marks those lines so you can tell them from game text. send types a command into the game, exactly as if you typed it: you see the > wave go out and the game's reply come back.

Put a script in a group

To put a script in a group, make this its first line:

#group {Combat}

The Scripts panel then shows the script under a Combat heading. The line changes nothing about how the script runs. It's the one line in a script that starts with #. You can type the group line yourself, or assign the group in the script editor. A group line you type yourself appears in the editor later.

Run a script by name

To run any script by name, type a period followed by its name, such as .hello or .mine. Text after the name becomes arguments, which are values the script can read. See Arguments. The period is for the command line. Inside a script, start another script with run hello, and hand it values the same way: run hello Marcus 3. A send .hello line is refused before the script starts, with a message pointing you to run.

  • If that script is already running, or paused, run leaves it alone, and the script goes on. A note says so in debug mode.
  • A script can't run itself. To start a script over, use the Scripts panel, or type the period and its name in the command line.

Script names

Capitalization doesn't matter in a script's name. A script's name is one word, because the words after it are its arguments. An older script with a space in its name still loads. The editor marks the name until you change it, and run can't start the script until you do.

How a script runs

  • Lines run in order, one at a time. A script never skips ahead or runs two lines at once.
  • A pause holds the script. A pause followed by a number holds the script at that line for that many seconds, until the wait ends. See Waiting.
  • A script ends when it reaches its last line, or hits stop. You can also stop a script yourself from the Scripts panel. A script can end another script by name, or every script; see stop.

Tip: If a script seems stuck, open the Script Output panel. The top of it shows the line the script is on and what it's waiting for.

Comments

A line that starts with // is a comment: a note for you, not for Merlin. Merlin skips it entirely; it changes nothing about how the script runs. It's the same mark in every file Merlin reads. A bare pause 6 doesn't explain why the script waits six seconds; a comment above it can.

// Wait out the post-combat cooldown before hiding
pause 6
send hide

A comment is always a whole line. To note a command, put the comment on its own line above it. Important: A line that starts with # isn't a comment. If a script has one, Merlin doesn't start the script, and tells you which line to fix. The group line, #group, is the only exception; see Put a script in a group.

stop: end this script, another script, or every script

stop on its own ends the script it's in. Put a script's name after it, and it ends that script instead. The script with the stop line keeps going.

stop Hunt
send sheathe sword

This reads as: end the Hunt script, then sheathe your sword. You see:

> sheathe sword
  • The name is the script's name as the Scripts panel shows it. Capitalization doesn't matter.
  • A name Merlin doesn't know is an error, and the script doesn't start.
  • If the named script isn't running, nothing happens, and the script goes on. A note says so in debug mode.

stop all ends every running script, including the one it's in. It does the same as Abort All Scripts.

action stop all when You are bleeding

This reads as: the instant the game says You are bleeding, end every script. Both forms work in an action body. A script can't be named all.

Output

Output is what a script produces: text you see, text the game sees, sound, or attention. These verbs are how a script speaks:

VerbEffect
send <text>Sends a command to the game
echo <text>Prints a message visible only to you
debug <text>Like echo, shown only when debug mode is on
play <sound>Plays a sound file
speak <text>Says the text out loud
beepPlays the system alert sound
flashFlashes Merlin's icon when you're in another app

To turn debug mode on, type /debug in the command line. Merlin answers Debug messages ON. To turn it off, type /debug again. Debug mode starts off each time Merlin opens.

speak: say it out loud

speak reads its text aloud in the system voice, the way a trigger's spoken alert does. It can say a value, which beep, flash, and play can't.

speak $monstercount wraiths

This reads as: say how many wraiths are in the room. With three in the room, the voice says "three wraiths," and the script goes on at once.

  • A trigger's spoken alert always wins. A speak line that arrives while an alert is speaking is dropped, and a note says so in debug mode.
  • The newest speak line wins over an older one still being said. The cut-off line is noted in debug mode too.
  • Nothing waits in line. When the script stops or ends, only the words already being said finish.
  • While Merlin is muted, nothing is said.
  • Missing and empty values: A value that's missing is an error on that line, and nothing is said. An empty value says nothing, quietly. speak with nothing after it is refused before the script starts.
  • A line is cut to 500 characters.
  • speak works in an action body: action speak $arg1 whenre ^(\w+) wanders in.

Waiting

After some actions, such as an attack, the game makes you wait a few seconds before you can act again. That wait is the roundtime, also called the delay. If your script ignores it, the script sends commands too early. The game then queues them and runs them later. Relying on the game's queue risks the script malfunctioning. To avoid this, use wait verbs. Each one is for a different situation.

The built-in safeguard

Merlin gives you one safeguard without asking. A script's next command isn't sent until the game has answered the last one, from any script, with its prompt. That means two send lines in a row go out one reply apart. This doesn't replace the wait verbs: it spaces commands; it doesn't wait out roundtime. A pause waits out a delay only if the game announces it before the pause ends. If the delay is announced later, the next command can go out during it, so choose a pause long enough to cover the action's roundtime. If the game ever fails to answer a command, Merlin says so after five seconds and sends the next one anyway. A delay the game has announced doesn't count as silence. A command sent during a delay is answered when the delay ends, so Merlin waits for that end, and five seconds more, before it says anything.

Pausing a script freezes its waits

A script paused in the middle of a waitfor, waitforre, waiteval, or matchwait doesn't run out of time while it's paused, however long you're away. When you resume it, it picks up where it was, and a line that arrived while it was paused still counts.

  • A paused script sends nothing, not even a command that was already waiting its turn.
  • A walkto in progress stops walking when you pause. When you resume, it asks the map for the same room again from wherever you stand.

pause (wait N seconds, plus any roundtime)

pause 3 waits at least 3 seconds. If the character is still in roundtime when the 3 seconds end, it keeps waiting until the roundtime is over.

  • Fractions work: pause 0.5 waits half a second.
  • The most is 600 seconds, as for every wait. A longer number is cut to 600, and the script says so.

Choose a pause length that comfortably covers the action's roundtime. For example, an attack can cause a 3- or 4-second roundtime (the game prints [4 sec. delay]), so pause 5 clears it with room to spare. Tip: Use pause by default, unless you have a specific reason to use another wait.

delay (wait exactly N seconds)

delay 3 waits precisely 3 seconds, ignoring roundtime. Use it when the wait has nothing to do with roundtime, for example, pacing out a line of dialogue or emotes so each one has time to be read. The most is 600 seconds, as for pause.

later (do one line N seconds from now, while the script carries on)

later 90 send stand schedules one line to run 90 seconds from now. The script doesn't wait. It goes straight on to its next line, and the scheduled line runs when its time comes.

later 90 send stand
send sit
waitfor You sit down
echo resting

This reads as: in 90 seconds, stand up. Meanwhile, sit and rest. sit goes out at once, "resting" appears when the game answers, and stand goes out 90 seconds after the later line was reached.

  • The seconds are a plain number from 0.1 to 600. A value isn't allowed there.
  • The line after the seconds is one instant line, the same kinds an action body takes: send, set, keep, math, echo, speak, stop, and the rest. No if, no wait, and no later inside a later.
  • The scheduled line's values are read when the later line is reached. later 10 echo $count uses the count as it was then, not as it is 10 seconds later. A missing value is an error on the later line, and nothing is scheduled.
  • One scheduled line per later line. If the script reaches the same later line again before it runs, the new one replaces the old one.
  • Pausing the script freezes the countdown. A scheduled send that's waiting out roundtime when you pause waits for the resume too.
  • A scheduled send waits out roundtime and takes its turn like any other command.
  • A script stays running until its scheduled lines run. When the script's lines are done, the script stays running until its scheduled lines have run. The top of the Script Output panel shows what it still has to do. Stopping the script cancels them, and so does the script's own stop line, an action stop, a stop from another script, a restart, or a disconnect.
  • A trigger's script line runs a fresh copy each time it fires. If it fires again while the last copy is still waiting on a scheduled line, the new firing waits its turn and runs once that copy ends, so both scheduled lines run. With skipifbusy, the new firing is skipped instead.

waitfor (wait for specific text)

send open sack
waitfor You open the closed

waitfor holds the script until a line from the game contains the given words, then continues.

  • It watches only text that arrives after the waitfor starts. Earlier lines don't count.
  • It matches whole words, with capitals exactly as the game prints them. waitfor rat isn't woken by You take a bite of your hard rations., and waitfor you open the closed isn't woken by You open the closed purse. The same goes for match, until, stopon, and action … when. To catch part of a word, or either capital, use waitforre, matchre, or action … whenre.
  • If the words never come, the script stops after 30 seconds and tells you what it was waiting for.
  • For something that takes longer, put the number of seconds first: waitfor 120 You fall asleep waits up to two minutes. For every wait that watches for words or a condition, the most is 600 and the least is 0.1; a number outside that range is clamped, and the script says so. pause and delay take any number from 0 up, with the same most of 600. Because the first word is read as the number, text that itself starts with a number needs a word in front of it: waitfor up 2 silver coins, not waitfor 2 silver coins.
  • A $name in the text that has no value ends the script and names the missing variable. Otherwise, waitfor $prey dies with no $prey would wait for the literal words $prey dies and eventually time out. The same goes for match rows, an action line's when, and the text of an until or stopon line, read where that line is reached, so a loop that sets the value first is fine. A send with such a value says so and sends the words as typed.

waitforre is the pattern-matching version. See Patterns.

Wait for every answer

A game doesn't always say yes. When you open a sack, it might open, it might already be open, or there might be no sack. A waitfor that waits for only one answer doesn't notice when the game gives another. It keeps waiting, and after 30 seconds it stops the script. Put every answer you know in one waitforre, with a | between them:

send open sack
waitforre You open|already open|I see no

The script goes on, whichever answer comes. To find the answers, look in your logs for times you typed the command yourself. To do something different for each answer, use a match table; see Multiple outcomes: match tables.

waiteval (wait for a condition)

waiteval $health > 80

waiteval $health > 80 reads as "wait until my health is above 80." The script pauses at this line and waits for the condition to be true. Once it's true, the script moves to the next line. Use waitfor for events the game states once. Use waiteval for values Merlin tracks continuously, such as health. The default timeout is also 30 seconds; on timeout, it reports both the condition and the value it last saw. To wait longer than 30 seconds, put the number of seconds first: waiteval 120 $health > 80 waits up to two minutes. Important: If nothing can ever make the condition true, the script waits until it times out. If a monster is still attacking you, your health won't rise while you stand there and wait. Use if to take action instead. See Conditions.

waitreply (wait for the game to answer the last command)

send skills
waitfor Remain
waitreply
if $remain(Swimming) = 0 then echo Swimming is ready to train

This reads as: ask for the skills list, wait for its header line, wait until the game has finished answering, then read it. $remain(Swimming) holds the number from the list the game just sent, never the one from before. waitreply waits for the prompt that ends the reply to the last command this script sent. That's the moment every value the reply filled can be read: the skills list, your wounds after health, the room after look. A waitfor, waitforre, or matchwait that finds its line gives waitreply a new starting point. waitreply then waits for the first prompt after that line. That's the sure form. Nothing you type and nothing Merlin sends on its own can end the wait early, unless it prints that same line, such as a skills you typed yourself. Wait for a line only the reply you want can print, then use waitreply.

  • Use it when you read a game variable right after the command that fills it. pause 1 is a guess that the answer took less than a second. A reply can take two seconds, and longer if the command went out during a delay.
  • Use waitfor instead when you want a particular line, and waitreply when you want the whole answer.
  • If this script has no unanswered command, or the answer has already come, waitreply doesn't wait at all.
  • A command sent by an action line or by a scheduled later line counts, once it has gone out. An action send still waiting out a delay isn't yet the last command. A script started with run sends its own commands, not this script's, and a walkto walk's steps are the map's.
  • Without a waitfor before it, Merlin waits for the first answer after the command went out. If anything else was sent since the game's last answer (a command you typed, one Merlin sent on its own, or one Merlin sent anyway after the game went 5 seconds without answering), Merlin can't tell whose answer comes first. waitreply then goes on at the first answer and says so once, naming the sure form. A command sent before the last answer and still unanswered isn't counted, so the sure form is the one to trust.
  • Wait for a line of the reply you want, not for any line. A line the game prints on its own, such as an attack, comes with no answer, and a waitreply after it waits for the next answer to anything, or until its patience runs out.
  • Pausing the script freezes it.
  • If the game hasn't answered after 30 seconds, the script stops and names the command. waitreply 60 waits up to 60 seconds. The number is plain, up to 600.

Moving

move (take a step, and know it worked)

move north
if $moved = no then echo the way north is blocked

move north reads as "go north, and tell me whether I got there." send north sends the word and moves on at once. It can't tell whether you arrived. move north sends the same word, then waits for the map to say you're in a new room, and only then continues. Merlin stores the result in $moved: yes if the room changed, no if it didn't. A no isn't an error. It means something kept you where you were: a creature blocked the exit, the exit wasn't there, or the game refused the step. The script continues to the next line, where if $moved = no decides what to do about it.

  • It waits 10 seconds for the room to change. For more, put the number of seconds first: move 20 north.
  • It reads the map, not the game's text, so it needs no list of wordings to watch for.

nextroom (wait for a move you sent yourself)

send swim east
nextroom

nextroom reads as "I've just sent a move; wait until I'm in the new room." Some moves aren't a direction word: swimming across water, go path, climbing a ladder. move sends those too. move swim east sends swim east and waits for the room to change. For a move you sent some other way, put nextroom on the next line to wait for the room to change. It fills $moved the same way move does, yes or no.

  • It waits 10 seconds for the room to change, like move, and leaves the answer in $moved.
  • nextroom 20 waits up to 20 seconds.
  • Nothing is sent by nextroom itself. It only waits.

walkto (let the map take you to a room)

walkto 9460

walkto 9460 reads as "take me to room 9460." The script gives the room to the map and waits while the map walks you there, then continues.

  • The number is the room's ID, the one $roomnum shows when you stand in it.
  • The map plans the route from wherever you are. The script doesn't need to know where it started.
  • A creature blocking the way doesn't stop it. The map waits out the roundtime and tries the same step again, as many times as it takes, and stands you up if you're knocked down. Important: Don't add retries of your own around walkto. Two layers of retry would send the step twice.
  • If the map can't get you there, the script stops and says why, with the room and the map's word for the reason, such as walkto 9460: timeout: unseen (the map doesn't know where you are), unreachable (no way there, or a room the map doesn't know), closed, locked or stuck (a door the walk couldn't get through, such as a locked door with no key; closed also when the map is closed during the walk), timeout (a step went unanswered), mismatch (a step took you somewhere the map didn't expect), too tired or can't swim (the game refused the step), dead, superseded (you moved yourself or clicked another room), stopped (you clicked the room you stand in), or atlas or wiped (you picked another atlas, or deleted the zone).
  • A walkto to the room you already stand in does nothing, and the script goes on.
  • Pausing the script stops the walk where it is. Resuming asks the map for the same room again, from wherever you stand, once the game has answered the step the pause interrupted. If no answer has come, it waits up to two seconds for one and then goes on.
  • A walk through creatures keeps retrying a blocked step, with no limit, and a creature blocking the way can attack you while it does. Give the script an action that stops it when your health drops, such as action stop when eval $health < 40.
  • The map must be open. A closed map answers "no map is open to walk" and the script stops there.

Variables

A variable stores a value under a name. Some come from your game, such as your health ($health). Others you name yourself. Write $health anywhere in a script to use that value.

echo My health is $health.

The result, printed only on your screen:

Merlin Client: My health is 87.

Two variables can work in the same sentence. Here the script sets its own target first with set; see Local variables:

set target a dark hound
echo My health is $health and I am fighting $target.

The result, printed only on your screen:

Merlin Client: My health is 87 and I am fighting a dark hound.

Each $name was replaced with its current value at the moment the line ran. There are three kinds of variables: game variables, saved variables, and local variables.

Game variables

Game variables come with the game's module in Merlin and take their values from the game. When the game reports your room, your health, or what's in your hands, Merlin keeps the latest report, and the variable is how a script reads it.

  • You can read them anywhere a script accepts text or a condition.
  • keep can't write them; the game updates them when it reports the information they hold. set can give a game variable a different value for the rest of that run.
  • Until the game has spoken, a variable isn't set at all. A condition that reads it stops the script and says why. A waiteval waits for it instead. Ask is set first if the game may not have spoken yet.

Which names exist depends on the game you're playing. To see the list for your game, with live values and what each one means, go to Settings > Variables and look under Merlin managed variables. For example, $target is the creature you last attacked, in the form the game takes back: hound, or 2nd hound when there are two. A hunting script can attack it without knowing its name ahead of time:

send attack $target

It's set by your own attacks and spells only, never by someone else's fight. If the creature ahead of yours dies, the game renumbers yours, and $target gets the new number from your next attack line. A script that says set target rat keeps its own rat for that run.

Game variables that take a name

Some game variables take a name in parentheses. A skill's numbers work this way:

send skills
waitfor Remain
waitreply
if $remain(Thrown Weapons) = 0 then echo Thrown Weapons is ready to train

The script asks for the skills list, waits for the list's header line, waits for the game to finish answering, then says so when Thrown Weapons has no uses left before it can train.

  • $remain(Thrown Weapons) is the uses left before the skill can train.
  • $level(Thrown Weapons) is the skill's level.
  • $ready(Thrown Weapons) is yes or no.

Write the skill's name the way the skills list shows it. Capitalization doesn't matter. The name in the parentheses can come from a variable. $remain($s) with $s holding Thrown Weapons reads $remain(Thrown Weapons). That lets one loop read every skill:

set skills Sharp Weapons|Blunt Weapons|Brawling
foreach s $skills
if $remain($s) = 0 then echo $s is ready
endeach

This reads as: for each skill on my list, say so if it's ready. Parentheses can mean two different things. For a value of your own, $list($n) uses $n as the position of an item in the list. For a game value that takes a name, such as $remain, $level, or $ready, $remain($skill) uses $skill as the skill's name.

A value of your own with the same name wins when the parentheses hold a $name. If you have your own $remain, $remain($skill) is treated as a list lookup. Because a list position must be a number, a skill name there is refused as not a number. Written out, $remain(Thrown Weapons) still reads the game's value. If the name is wrong, Merlin says so and suggests the right one:

no skill called "Throw Weapons" — did you mean Thrown Weapons?

Saved variables

Saved variables are stored on the computer where you created them. To create one by hand, go to Settings > Variables. A keep line in a script creates or updates one:

keep kills 12

Settings > Variables now shows a row kills with the value 12, and $kills reads 12 in any script, today or next week. Saved variables persist across sessions and across scripts on that computer: quit Merlin, reopen it next week, and the value is still there.

  • Game variables can't be written with keep; Merlin reports an error if you try.
  • Merlin's own names can't be written either: args, arg1 to arg9, match, moved, unixtime, date, time, and any name starting with timer_. The same rule holds for set and every other line that names a variable.

Local variables

Local variables are useful for holding a value you'll reuse a few lines later. Create a local variable with set:

set prey rat
send kill $prey

The result:

> kill rat

A local variable isn't stored between runs. A line such as set prey rat creates it fresh each time the script runs. It doesn't appear in Settings > Variables, and its value lasts only for that run. set keeps everything after the name, spaces and all. If $gem holds quartz gem, then set found cracked $gem stores cracked quartz gem. That's how you join words.

Variable names

  • Variable names use letters, numbers, and underscores.
  • arg1 through arg9 and args are reserved for script arguments; Merlin rejects any attempt to use them as another kind of variable.
  • Variable names can't start with a number.

Arguments

Extra words after a script's name become its arguments: values the script can read. .heal Marcus runs the heal script with Marcus as an argument. $arg1 holds the first word, Marcus. $args holds everything after the script's name. Inside a script, run heal Marcus does the same thing.

send cast heal $arg1

The result of running .heal Marcus:

> cast heal Marcus

Pattern captures can temporarily replace $arg1. A capture from matches or an action … whenre body lasts only for that line. A capture from a matchre row lasts for the rest of the run, or until the section that caught it returns. After the capture expires, $arg1 goes back to the value it held before. A variable is one argument, even when its value has a space in it:

set patient Marcus Taath
run heal $patient

heal runs with $arg1 holding Marcus Taath.

Name the arguments

To give the words names, start the script with args. The names are taken in order. Write them without a $, the same as set: you name a variable here, and read it with $ everywhere else.

args prey retreat
send attack $prey

.fight hound south gives $prey the word hound and $retreat the word south. $arg1 and $arg2 still work too.

  • If you type fewer words than there are names, the extra names are empty.
  • A section you gosub to can start with its own args line to name the words its gosub handed it.
  • A name Merlin already uses for the game, such as health or roomnum, is refused before the script starts, with a message saying so. An argument with that name would hide the real value for the whole script. If you really do mean to replace a game value inside your script, use set instead.

The clock

Three variables read the clock on your computer. They're the same in every game.

  • $time reads the time of day, 24-hour, to the minute. Example: 14:05.
  • $date reads the date. Example: 2026-10-19.
  • $unixtime reads whole seconds since 1970, a number. Example: 1792612345.
echo $time: killed $target

At five past two in the afternoon, with $target holding wraith, 14:05: killed wraith appears on your screen. $time and $date are text. Compare them with = or !=, and write the other side in the same format: if $time = 03:00 then send say it is three. A condition such as $time > 03:00 is refused before the script starts. To ask how long, use $unixtime, which is a number:

math until = $unixtime + 60
waiteval 120 $unixtime >= $until
send say a minute has passed

The script waits sixty seconds, then sends the line. The number after waiteval is how long it may wait. Without one, a wait gives up after 30 seconds, before the minute is up. Note: When your clock skips an hour in spring, 02:30 never comes that day. When it repeats an hour in fall, 02:30 comes twice. A clock set forward or back moves $unixtime by the same amount.

Conditions

if $health < 50 then send flee

if $health < 50 then send flee reads as "if my health drops below 50, flee." The shape is: if <condition> then <line>. An if on one line controls one verb: the condition, then then, then the verb. To run several lines under one condition, use a block. See Several lines under one condition, below. With $health at 42, > flee goes out. At 70, nothing happens, and the script moves to the next line.

OperatorMeaning
=Equals (capitalization doesn't matter)
!=Not equals
< > <= >=Numeric comparison
contains starts endsPart of a value (capitalization doesn't matter)
matchesA pattern test, with captures for that line
is set is not setWhether a variable holds anything
and or notCombine conditions
( )Group

in tests how a value ends against a list (see Matching against a list). To react to words inside a line the game prints, use a pattern: waitforre, matchre, or action … whenre.

Combine conditions

if $hidden = yes and $health > 80 then send stalk
if not $target = rat then echo New prey!

if $hidden = yes and $health > 80 then send stalk reads as "if I'm hidden and my health is above 80, stalk." if not $target = rat then echo New prey! reads as "if my target isn't a rat, echo New prey!"

Put words with spaces in quotes

Words you type with a space between them go in quotes. A variable needs none: it's one value, whatever it holds.

if $roomname = "The Yard" then send home

if $roomname = "The Yard" then send home reads as "if I'm in The Yard, go home." Without the quotes, Merlin sees two words where it expects one and refuses the script before it starts. Room names nearly always have spaces, so this is the shape for any test on $roomname. Quotes around a variable, "$list", compare it as plain text. Without them, a value holding | is read as a list, and a list can't be compared with =.

Empty variables

An empty variable compares as empty. set target with nothing after it leaves $target empty, and if $target = none is simply false. An empty variable counts as false in a condition by itself. If $flag is empty, if $flag then … does nothing.

Test part of a value: contains, starts, and ends

contains, starts, and ends test part of a value. Capitalization doesn't matter.

if $prize contains dagger then send get $prize
if $roomname starts The then echo Still in town.

if $prize contains dagger then send get $prize reads as "if the thing I found has dagger anywhere in its name, get it." With $prize holding throwing dagger, > get throwing dagger goes out. With bronze battle-axe, nothing happens. if $roomname starts The then echo Still in town. reads as "if the room's name begins with The, echo Still in town." With starts, contains and ends, words with spaces don't require quotes: if $roomname starts "The Yard" works with or without them. The words contains, starts, ends, and matches are reserved, like and, or, not, and in. A value that's one of those words goes in quotes.

Test a pattern: matches

matches tests a value against a pattern, the same kind waitforre takes; see Patterns. The pattern goes in quotes. What the pattern's parentheses catch is in $arg1, $arg2, and so on, for the rest of that line only.

set line You found 47 silver coins
if $line matches "(\d+) silver" then set coins $arg1
echo $coins

This reads as: if the line holds a number before the word silver, keep that number as coins. The result, printed only on your screen:

Merlin Client: 47
  • Capitals matter in a pattern. matches "Silver" doesn't find silver. The other tests ignore capitals.
  • The captures last for that line's then part and no longer. The next line reads the script's own $arg1 again, and an else never sees them. To keep one, store it: then set coins $arg1. Only the pattern's own groups are filled: with two groups, $arg3 and $args stay the script's own.
  • A block, if … matches … {, gets no captures. A line inside it that reads $arg1 is refused before the script starts. Store the capture on a then line first.
  • A $name inside the pattern is replaced with its value before matching. The value is matched as plain text, so a . or * in it means only that character. A quote inside the pattern is written \x22.
  • A pattern Merlin can't read, or one with a named group, is an error before the script starts. A pattern keeps at most nine groups.
  • One matches per line when its captures are used. A line that reads $arg1 after two of them, or after a matches joined by or or not, is refused before the script starts, because a true condition couldn't say which match filled them.
  • In waiteval, until eval, stopon eval, and action … when eval, matches is a plain yes or no and keeps nothing. To capture from the game's text, use whenre or matchre.

Ask whether a variable holds anything: is set

is set asks whether a variable holds anything at all:

if $target is set then send attack $target

if $target is set then send attack $target reads as "if I have a target, attack it." A variable never set, or holding nothing, isn't set. is not set is the reverse. Important: if $food on its own works with yes or no, true or false, and on or off. Numbers work too: 0 is no, and any other number is yes. An empty value is no. If it holds a word such as bread, the script stops. To ask whether a variable holds anything, use is set:

if $food is set then send eat $food

When a condition can't be read

A condition that can't be read stops the script. if $gone = yes with $gone never set is neither true nor false, so the script stops on that line and says why, the way it stops on a walk that didn't arrive. This is on purpose. A skipped line would let the script carry on past a check it couldn't make. Give every variable a value before a condition reads it, or ask is set first.

  • is set reads a never-set variable as no.
  • and and or stop reading at the first side that decides, so if $target is set and $health < 40 is safe when there's no target.
set gone no
action set gone yes when wanders away

This reads as: start with gone as no, and let the action line turn it to yes.

else: do something when the condition is false

if $health > 50 then send attack hound
else send flee

else runs a line only when the if directly above did nothing. This reads as "if my health is above 50, attack; otherwise, flee." Exactly one of the two lines runs. With $health at 90, > attack hound goes out. At 42, > flee goes out instead. Chain them for more than two outcomes:

if $health > 80 then send attack hound
else if $health > 30 then send parry
else send flee

This reads as: above 80, attack; otherwise above 30, parry; otherwise flee. One line runs, never more. With $health at 55, > parry goes out. The first line did nothing, the second matched, and the third never ran.

  • else sits directly below its if. Blank lines and comments in between are fine.
  • The line after then is one verb that does its work on that line, such as send, set, echo, or gosub, which runs a whole section. A line that reaches past itself can't follow then: repeat, until, stopon, foreach, endeach, waiteval, the match lines, action, include, and args. Put those under a block instead, except include, which goes on a line of its own outside any block; see Several lines under one condition.

Several lines under one condition

Put the lines between braces. The { is the last word of the if line, and the } sits alone on a line of its own. There's no then.

if $want = thrown {
  send unready shield
  waitfor You remove your
  pause 1
}
else {
  send ready $sharp
}

This reads as: if I want the thrown weapon, unready the shield, wait for the game to say so, and pause; otherwise, ready the sharp one. With $want holding thrown, the three lines in the first block run, one after another, and the else block is skipped. With anything else, only ready goes out. else if $hp > 30 { makes a ladder, the same way as the one-line form. Exactly one block runs. Blocks can sit inside blocks, and inside repeat and foreach loops.

  • The lines inside a block run one at a time, so the game can change between them. The condition is read once, at the {. If a later line must check again, give it its own if.
  • Spaces at the start of a line mean nothing. They're there for your eyes.
  • A { that isn't the whole end of an if line is ordinary text, so send emote {grin} sends emote {grin}.

An if … { and its else go together as blocks: after a }, write else { or else if … {, not a one-line else. A section name (Name:) goes outside the braces, never inside. These mistakes are reported before the script starts.

Loops

A loop runs the same lines more than once, so you don't write them out by hand for each repetition.

repeat / until: run N times

repeat 5
send buy apple
pause 1
until

This runs the block 5 times. A bare until just closes the block.

  • repeat takes a whole number from 1 to 1000. A number outside that range stops the script from starting.
  • A variable works too: repeat $times. The variable must hold a whole number from 1 to 1000. Anything else ends the script and reports the value that caused the problem, including an empty or unset variable.
  • Loops can sit inside loops; each until closes the nearest open repeat.
  • A stopon ends only the loop it's written in, and it only looks at lines that arrived after that loop began.

repeat with no number: run until told to stop

repeat
send drink canteen
pause 1
until You're no longer thirsty

This reads as "keep drinking until the game says I'm no longer thirsty." With no number, the loop has no end of its own. It ends when its until text arrives, when a stopon fires, or when you click Stop. Sips go out one a second until the game prints You're no longer thirsty., then the script moves on. Important: A loop with no number must wait for something on every turn: a pause, a waitfor, a waiteval, or a command sent to the game. A loop that only sets variables runs continuously without waiting. Give every loop a wait. repeat $times never becomes a loop with no end. An empty $times ends the script instead.

End a loop early with until

Give until matching text to end the loop early, once the game confirms completion:

repeat 20
send drink canteen
pause 1
until You're no longer thirsty

This reads as "repeat up to 20 times, but stop as soon as the game says 'You're no longer thirsty.'" The result of one run, where the game confirmed on the seventh sip:

> drink canteen
You take a drink from your small canteen.
   ...six more sips...
> drink canteen
You take a drink from your small canteen.
You're no longer thirsty.

The loop ended there, thirteen sips early. It stops at the until line once the text has appeared, or after 20 times, whichever comes first. Tip: Prefer ending on the game's own confirmation over counting turns.

Abandon a loop with stopon

stopon <words> abandons the current loop when the script reaches the stopon line, if its text has appeared since the loop began. It doesn't cut a wait short. It's for the unusual case, not the loop's normal exit. Outside a loop, stopon ends the whole script, just like stop: any scheduled later line is canceled, and action lines are switched off. Both until and stopon can use a condition instead of matching text: until eval $monstercount = 0, stopon eval $health < 30. They use the same conditions as if, and check the condition at the same point in the loop where they would otherwise check for matching text.

repeat 20
send drink canteen
pause 1
stopon claws at you
until You're no longer thirsty

The result, attacked on the third sip:

> drink canteen
> drink canteen
> drink canteen
The dark hound claws at you!

The loop stopped there, seventeen sips short of the limit, without ever seeing "You're no longer thirsty." stopon fires on its own text, independent of until.

foreach / endeach: run once per list item

A variable can hold a list. Separate each item with |, a vertical bar. It marks where one item ends and the next begins.

set snacks apple|bread|cheese
foreach snack $snacks
send eat $snack
pause 2
endeach

This reads as "for each item in $snacks, held one at a time as $snack, eat it and pause 2 seconds." The result:

> eat apple
> eat bread
> eat cheese

This runs the block once per item, with $snack set to the current one. Extending the list doesn't require changing the script.

List reference

  • $snacks(2) is the second item, bread. Counting starts at 1, not 0, so $snacks(1) is apple.
  • math n = items($snacks) counts the items in $snacks and stores the result in $n. For the snacks list above, $n becomes 3.
  • An item can contain multiple words: iron sword|healing potion is two items.
  • Most list operations use up to 100 items. If a list contains more, its full value is still kept, but foreach, items(), and $list(n) use only the first 100 and say so. list remove is different: on a list with more than 100 items, it keeps only the first 100 and drops the rest. list add won't add another item once the list has 100. The in test is different: it checks every item, even beyond the first 100.

list: add an item, or remove the best-matching item

list changes a list the script holds. add puts an item on the end. front puts it at the front. remove takes one item off.

set seen
list seen add rat
list seen add dark hound
list seen front Tam
echo $seen

This reads as: start with an empty list, add rat, add dark hound, put Tam at the front. The result, printed only on your screen:

Merlin Client: Tam|rat|dark hound

An item is one value. It can be several words, keeps its capitals, and may appear twice. A value with a | in it is refused, because that would make two items. remove uses the same rule as in: the item you name is matched by its ending, and the most exact entry goes first.

set wanted ruby|gem|quartz gem
list wanted remove cracked quartz gem
echo $wanted

This reads as: remove the list entry that best matches the end of cracked quartz gem. Two entries match, gem and quartz gem, so the more exact one, quartz gem, is removed. The result, printed only on your screen:

Merlin Client: ListDemo, line 2: list wanted remove took "quartz gem" for "cracked quartz gem"
Merlin Client: ruby|gem

When the entry taken isn't the exact text you gave, Merlin says which one went, so a wider removal is never hidden. When nothing on the list fits, nothing changes, and a note says so in debug mode.

  • list changes the script's own copy, like set. To change a list that comes from a saved variable, first copy it into the script with set wanted $wanted. Make your changes there, then use keep if you want to save the changed list.
  • A game variable can't be edited. Copy it to a name of your own first.
  • To empty a list, write set wanted with nothing after the name.
  • list works in an action body: action list seen add $arg1 whenre ^(\S+) wanders in.

Matching against a list: in

set wanted ruby|pearl|gem
if $item in $wanted then send get $item

This reads as "if my item is one of the wanted kinds (ruby, pearl, or something ending in gem), get it." in doesn't require the whole value to equal a list entry. It matches when the value ends with a list entry, word for word.

ValueEnds withMatch
cracked quartz gemgemYes
shining rubyrubyYes
ruby ringringNo
pearl buttonbuttonNo

Because item names put the category last, this gives you two things for free:

  • Adjectives don't need separate list entries. "Cracked," "shining," and "flawless" all match, whatever word comes before the kind.
  • A bare category word, gem, matches anything ending in it, including a kind you haven't found yet. quartz gem, sapphire gem, or any gem type you find later all match, since each ends in gem.

Important: When acting on a matched item, use the game's own full name for it, captured from its output (see Patterns), not your list entry. Games require exact names for actions; decide with the list, act with the captured name.

Sections

A section is a named block, called with gosub and ending in return:

send knock

gosub Greet
echo All done.
stop

Greet:
send bow
send say Good day!
return

This reads as: knock, then run the Greet section (bow, then say Good day!), then return to the line after gosub and continue: echo All done, then stop. The result:

> knock
> bow
> say Good day!
Merlin Client: All done.

gosub Greet runs the Greet: section, then returns to the line after the call, which is why "All done" prints last, after the section's two commands. Important: Put stop before the first section. Without it, the main script can continue straight into the section instead of waiting for a gosub to call it.

  • A section name is one word: letters, numbers, and underscores, starting with a letter or an underscore. Capitalization doesn't matter: gosub greet finds Greet:.
  • A section can call another, up to 8 deep. The ninth call is refused with a message, and the script goes on with the next line. A section that calls itself gets there fast. run has the same limit.

Hand a section a value

gosub can hand a section a value. Words after the section name become that call's arguments, read inside the section as $arg1 and $args; see Arguments:

gosub Heal Marcus
echo Done helping.
stop

Heal:
send cast heal $arg1
return

This reads as: run the Heal section with Marcus as its argument (cast heal on Marcus), then return and continue. The result:

> cast heal Marcus
Merlin Client: Done helping.

After return, $arg1, $args, and the script's other arguments return to the values they had before the gosub. A gosub with nothing after the section name works exactly as before. A return with no gosub to return to ends the script and reports the line. The same happens if a section reaches return while a repeat inside that section is still open because its until hasn't been reached.

include: share sections across scripts

Copy the same section into several scripts, and each copy has to be fixed by hand if something changes. Fix one copy and forget another, and they no longer do the same thing. Instead, put shared sections in one script, a library, and reference it:

include MyToolbox

This lets the current script call any section in MyToolbox with gosub, as if that section were written directly in the current script.

  • A library can only contain sections, comments, include lines, and its group line. Any other line outside a section is an error.
  • Nothing in a library runs on its own: a section only runs when another script calls it with gosub.

Math, text, random, timers

math: arithmetic on a variable

math works out a sum and stores the answer. Write the name, an equals sign, and the sum.

set kills 0
math kills = $kills + 1

This reads as "kills becomes kills plus one." A sum can combine values:

math percent = ($hp * 100) / $hpMax
In a sumWhat it means
+ - * /Add, subtract, multiply, divide.
modThe remainder after dividing. $n mod 10 is 0 every tenth time.
( )Work out the inside first.
round($x)Round to a whole number. Halves go away from zero: 2.5 becomes 3, -2.5 becomes -3.
abs($x)Make the number positive. abs(-5) is 5, and abs(5) stays 5.
min($a, $b)The smaller. min($hp, 100) is never above 100.
max($a, $b)The larger. max($dmg, 0) is never below 0.
items($list)How many items a list holds.
length($text)How many characters the text has, spaces included.

Multiply, divide, and mod happen before add and subtract, and parentheses come first. Spaces around the signs mean nothing: math n=$n+1 is the same line.

set snacks apple|bread|cheese
math n = items($snacks)
if $n = 0 then echo The snack list is empty!

This reads as: count the items in $snacks and store the count in $n; if $n is 0, echo that the snack list is empty. With $snacks set to apple|bread|cheese, $n is 3, and the echo stays silent.

  • A sum holds numbers only. + never joins words; set does that.
  • Every name in the sum must have a value. An unset one stops the sum and says which: math kills = $kills + 1 with no kills yet says set kills 0 first.
  • If a value in the sum isn't a number, the calculation fails and Merlin reports the value it found. Dividing by zero does the same. The variable keeps its previous value.
  • Decimals work: math price = $price / 3 on 10 gives 3.333333333333. A result too large to count exactly, or smaller than a trillionth without being zero, is refused.
  • % is refused, because it means percent in this client. Write mod.
  • A game variable can't be the name on the left. Put the sum in a name of your own.
  • A sum with a mistake in its shape is an error before the script starts.

text: change words

text takes some text, changes it in one way, and stores the result in a variable.

OperationWhat it does
shortenDrops the first word, keeps the rest.
firstKeeps only the first word.
lastKeeps only the last word.
trimTakes away extra spaces: at the ends, and doubled ones inside. a b becomes a b.
upperMakes every letter uppercase.
lowerMakes every letter lowercase.
replace "old" "new"Replaces every occurrence of the old text with the new text.
ordinalTurns a number into 1st, 2nd, 3rd.
oppositeGives the opposite direction.
text short shorten $prize

text short shorten $prize sets $short to $prize with its first word removed. If $prize is throwing dagger, $short is dagger.

text kth ordinal $k
send disarm $kth target

text kth ordinal $k turns a number into the way the game counts: 1st, 2nd, 3rd, 4th, 11th, 21st. If $k is 2, $kth is 2nd, and the script sends disarm 2nd target.

text back opposite $dir
send $back

text back opposite $dir gives the opposite direction. If $dir is north, $back is south. A short direction gives a short answer: ne gives sw.

text kind last $prize
send get $kind

text kind last $prize sets $kind to the last word of $prize. If $prize is bronze battle-axe, $kind is battle-axe, and the script sends get battle-axe. first keeps the first word instead: bronze.

text loud upper $arg1
send shout $loud

text loud upper $arg1 makes every letter a capital. If $arg1 is help, $loud is HELP, and the script sends shout HELP. lower goes the other way: HELP becomes help.

text mine replace "the" "Belka's" $item

text mine replace "the" "Belka's" $item sets $mine to $item with every the turned into Belka's. If $item is the rusty dagger, $mine is Belka's rusty dagger.

  • Capitalization doesn't matter in the old part; The is found too.
  • The new part goes in exactly as typed. An empty new part, "", deletes the old part.
  • Both quoted parts are required, and a replace missing one is refused before the script starts.

If the input doesn't work, such as 0 for ordinal or sideways for opposite, Merlin reports it and leaves the variable unchanged. A missing input variable is handled the same way: Merlin reports it, and the variable being set keeps its old value.

random: a random integer

random d 1 6

random d 1 6 sets $d to a random whole number between the two bounds you give it. Here that's 1 through 6, like a die roll. Use it to make repeated actions feel less mechanical, with different words and different pauses each time:

random r 1 4
if $r = 1 then send emote hums an old marching tune.
if $r = 2 then send emote stretches and cracks her knuckles.
random rest 3 8
pause $rest

This reads as: roll 1 through 4. On a 1, hum a tune; on a 2, crack her knuckles; on a 3 or 4, do nothing. Then roll a rest of 3 to 8 seconds and pause that long. The result of one run, where a 2 and a 6 came up:

> emote stretches and cracks her knuckles.

That's followed by a 6-second pause. On a 3 or 4, nothing is sent at all. Every run rolls fresh.

timer: elapsed-time tracking

timer start errand

timer start errand starts a timer named errand. Merlin tracks its elapsed seconds in a variable built from that name: timer_ plus whatever you called it, so $timer_errand.

if $timer_errand > 300 then echo five minutes on this errand

This reads as "if the errand timer has run past 300 seconds, say so." Five and a half minutes after the start, $timer_errand reads 330, the condition is true, and the message appears. timer clear errand removes the errand timer. Multiple named timers can run at once.

turn on, turn off: switch a scheduled timer

A scheduled timer is one from Settings > Timers, which sends a command every so many seconds; see the Timers Manual. A script can turn a scheduled timer off and back on by name.

turn off timer StandUp
send sit
pause 60
turn on timer StandUp

turn off timer StandUp reads as "turn off the timer named StandUp." turn on timer StandUp turns it back on. Capitalization doesn't matter in the name.

  • A timer that's already on is left alone. Its countdown doesn't start over.
  • Switching one timer never restarts another timer's countdown.
  • A name no timer has is reported, and the script keeps going.

hold, release: keep a trigger group off while the script runs

A script can temporarily turn off a trigger group while it handles that job itself. hold trigger group turns a group off for as long as the script runs. The group comes back the moment the script ends, however it ends.

hold trigger group Healer
walkto 184
send shop heal
waitfor restored to full vitality

This reads as: keep the Healer triggers off, walk to the healer, buy the heal, and wait for the game to confirm the healing. When the script ends, the Healer triggers are back on. The result:

> shop heal
You are healed and restored to full vitality!

release trigger group Healer turns the group back on before the script ends.

  • The name is the group's name as Settings > Triggers shows it. Capitalization doesn't matter. A name no trigger carries is an error, and the script doesn't start. A name that comes from a value is checked when the line runs, and a bad one ends the script with a message.
  • A hold isn't saved. It changes nothing in the triggers file, and your own Pause and Disable settings stay as they are.
  • While any running script holds a group, the group is off. Two scripts can hold the same group, and it comes back when the last of them ends. A paused script keeps its hold.
  • A trigger that watches a value keeps watching while its group is held. If the value crosses its trigger point during the hold, that crossing is not saved to fire later when the group comes back on.
  • Releasing a group the script doesn't hold does nothing. A note says so in debug mode.
  • Both lines work in an action body.

Multiple outcomes: match tables

waitfor handles one expected response. Some commands can produce several different responses. A swing at a creature might land, or miss. A match table handles all of them:

match Hit The swing is a
match Miss The swing misses
send attack hound
matchwait

Hit:
echo A hit!
stop

Miss:
echo A miss.
stop

This reads as: register two possible outcomes. If the game says The swing is a, jump to Hit; if it says The swing misses, jump to Miss. Then attack, and matchwait holds until one of them appears. The result when the swing lands:

> attack hound
You swing your bronze battle-axe at the dark hound!
[4 sec. delay]
The swing is a strong blow to the head.
Merlin Client: A hit!

The result when it doesn't:

> attack hound
You swing your bronze battle-axe at the dark hound!
[4 sec. delay]
The swing misses its target.
Merlin Client: A miss.

The shape is: one match <section> <words> line per possible outcome, then the triggering command, then matchwait.

  • Matching starts the moment match runs, not when matchwait does. A reply that arrives before the script even reaches matchwait still registers.
  • The first matching outcome wins; the rest are canceled.

There are two ways to end matchwait:

  • matchwait alone waits for one of the outcomes declared with match to appear. If none does within 30 seconds, it times out and stops the script.
  • matchwait 10 waits up to 10 seconds. If an outcome appears, it jumps to that section immediately. If none appears, the timeout is a normal outcome, not an error: the script continues with the line after matchwait, and none of the sections run.

matchre works like match, but with a pattern instead of plain text. Anything the pattern captures becomes $arg1, $arg2, and so on from the section it jumps to onward (see Arguments for how long they last), which is useful for pulling a name or number out of the game's text. See Patterns for a worked example.

Patterns

Plain text in waitfor or match has to match exactly, word for word. A pattern, also called a regular expression, matches text that changes each time, such as a number. One rule catches "You pick up 2 silver coins" and "You pick up 15 silver coins" alike. The symbols:

  • text matches itself.
  • | means or.
  • \d matches any digit.
  • + matches one or more of the thing before it.
  • . matches any character.
  • \. matches a literal period.
  • \[ and \] match literal brackets.
  • ( ) is a capture group.
waitforre opens|unlocks
waitforre You pick up \d+ silver coins
waitforre You're no longer thirsty\.

waitforre opens|unlocks reads as "wait for 'opens' or 'unlocks.'" waitforre You pick up \d+ silver coins reads as "wait for 'You pick up' followed by one or more digits, then 'silver coins.'" waitforre You're no longer thirsty\. reads as "wait for exactly 'You're no longer thirsty.'" The \. is a literal period, not "any character." Square brackets are special: [Yy] means any one of the letters inside. To match a bracket itself, put \ before it: \[\d+ sec\. delay\] matches [4 sec. delay].

Capture text

In matchre, action … whenre, and matches, parentheses capture: the matched text becomes $arg1, $arg2, in order. waitforre only waits, and keeps nothing.

matchre Found You pick up (\d+) silver coins
send get coins
matchwait

Found:
echo I picked up $arg1 silver!
stop

This reads as: register a pattern named Found that matches "You pick up" followed by a number, capturing the number. Send get coins, then matchwait holds until it appears. When it does, jump to Found and echo the captured number. The result when the game replies You pick up 2 silver coins.:

Merlin Client: I picked up 2 silver!

The parentheses grabbed the 2; the section received it as $arg1. Tip: Build a pattern by starting from an actual line the game printed, then replacing only the parts that vary. Capital letters must match, the same as in waitfor. To accept either, put both letters in brackets: [Yy]ou. An invalid pattern is rejected at script start, with its line number, not silently at run time.

What a pattern must obey in Merlin

A Merlin pattern is an ordinary regular expression, the same kind a web browser uses. Every symbol that works there works here, except that you can't add flags such as ignore-case. Use [Yy] instead. The rules below aren't about the symbols. They're about how Merlin uses a pattern, and a pattern that breaks one of them never matches, even when it's written correctly. They apply to waitforre, whenre, matchre, and a trigger set to matches a Regex.

  1. Merlin checks one line at a time. Each line the game prints is tested by itself. ^ means the start of that one line, and $ means its end. A pattern can't reach across two lines.
  2. Capital letters must match. opens doesn't match "Opens". To match either, write [Oo]pens.
  3. Captured text is stored in $arg1, $arg2, and so on. The first pair of parentheses is $arg1, the second is $arg2. Giving a group a name does nothing, except in matches, where it's an error. To group without saving, write (?: instead of (, and it takes no number. waitforre keeps nothing; to keep what a pattern finds, use matchre or whenre.
  4. $name in a script pattern is a variable. Merlin swaps in its value before the pattern is used, so $firstname becomes the character's name. A $ on its own at the very end still means end of line. A trigger's pattern is different: it's matched as written, so $name is never swapped for a value. There, $ means the end of the line, as in any regular expression.
  5. Start from the real line. Copy the text the game printed and change only the part that varies. A pattern written from memory can easily miss because the game's wording may not be exactly what you remember.
  6. Match the part that matters. just broke is enough to catch Your damaged short simple dagger just broke!, and it still works when the weapon changes. Put ^ at the front when the same words could turn up in the middle of some other line.
  7. Avoid patterns that repeat something that can also repeat inside itself. (\w+\s?)+ looks harmless but is slow in many pattern engines. Merlin protects you from most of these, and switches off any pattern that still runs too long, for the rest of the session, and names it. A script using that pattern stops with a message.
  8. A pattern with a mistake in it is refused. A script says so when it starts, with the line number. A trigger with one never fires. The trigger editor marks the pattern in red, and pointing at it shows what's wrong.

Regex tells you what ^ means. Rule 1 tells you which line it's the start of.

Background triggers: action

Some events need a response no matter which line the script is on: getting knocked down, receiving a whisper. action creates a temporary trigger, a watcher that checks every game line while the script runs:

action send stand when You stumble and fall down
action flash when whispers
action beep when whispers

This reads as: whenever the game says something matching You stumble and fall down, send stand. Whenever it says something matching whispers, flash Merlin's icon and beep. Both actions respond to the same matching line. The result, mid-script, whatever line it was on:

    You stumble and fall down!
> stand

The script never left its place; the action answered on its own.

Three trigger types

  • action ... when <words> fires when a game line contains the text. action send ready torch when too dark to see anything
  • action ... whenre <pattern> is the same as when, but matches a pattern instead of exact text. Anything the pattern captures becomes $arg1, $arg2, and so on, just like matchre. action echo Picked up $arg1 silver! whenre You pick up (\d+) silver coins
  • action ... when eval <condition> fires once when the condition becomes true, and can't fire again until it goes false first. A condition that's already true when the action line runs doesn't fire until it goes false and then true again. action send eat bread when eval $health < 30

A trigger in Settings > Triggers can watch a value too, and keeps watching when no script is running (see Watch a variable with If a value in the Triggers Manual). If a script's action and a trigger both watch the same value, both act at the same moment, so pick one of them for each job.

Group actions

Actions can share a group name, so several can be turned off, turned back on, or removed together instead of one at a time. Add the group name in parentheses right after action:

action (guards) send stand when You stumble and fall down

This tags the action with the group name guards. Any other action tagged (guards) joins the same group.

  • action (guards) off disables every action in the guards group.
  • action (guards) on re-enables them.
  • action (guards) clear removes them completely.

Actions belong to the script that created them. They stop when the script stops and pause when it pauses.

Keep an action to one instant effect

Keep an action's body to one instant effect: a send, an echo, a beep, a variable set, or starting a script with run.

  • A send in an action goes out after the line that fired it is on your screen, and after any delay, the same as pause 0 followed by send.
  • One command per action: if the text matches again while the first command is still waiting to go out, the second match is dropped.
  • A command still waiting is dropped when the script stops, pauses, or ends.

The one exception to "instant" is stop:

action stop when You are bleeding

This reads as: the instant the game says You are bleeding, end this script, wherever it is, even in the middle of a long wait. Getting away is then another script's job: an action run on the same text, placed before the stop, or a trigger that runs your Flee script. For anything more, have the action set a variable and check it from the main script:

set panic no
action set panic yes when eval $health < 30
// the rest of the script
if $panic = yes then echo health is low

This reads as: start with panic as no; the instant health drops below 30, set it to yes, no matter where the script is. Later, the main script checks $panic and says so if it's set.

Start a script from an action

An action can start a script:

action run Flee when You are bleeding
action stop when You are bleeding

This reads as: the instant the game says You are bleeding, start the Flee script, then end this one. The run line comes first, because nothing acts after a stop.

  • Flee starts after the rest of that batch of text, the same as a script a trigger starts, so its own first waitfor never sees the line that started it.
  • If Flee is already running, or paused, the action leaves it alone, and a note says so in debug mode.
  • Values after the name are handed over as with any run, captures included: action run Loot $arg1 whenre ^(\w+) is dead.
  • A name no script has, a $ in the name, and a script starting itself are errors before the script starts.
  • Thirty starts in ten seconds switch the action off for the run, with a message.

Validation

Before running, Merlin checks the whole script: section references, patterns, numbers, verb spelling, and matched pairs (repeat/until, foreach/endeach). It also refuses:

  • A section defined twice. The message names both lines, and the library if one came from an include.
  • A waitfor or waitforre with nothing after it.
  • A text … replace without both its quoted parts.
  • A $ on the name side of set, keep, math, text, random, timer, or foreach. You name a variable without the $ and read it with one.

A script with an error doesn't start. The error names:

  • The line number
  • The exact text that failed
  • What was expected

For example:

Hunt, line 12: no section named "Recovr" — gosub needs a line reading "Recovr:" somewhere in this script
Hunt: not started — fix the line above

Validation doesn't check variable values. Those come from the game and change at runtime, so pause $resttime always passes validation, whatever $resttime holds later. Runtime behavior follows the same principle:

  • Every wait has a timeout and reports what it was waiting for. walkto is the one exception: it keeps retrying a blocked step, and a creature blocking the way can attack you meanwhile, so give the script an action that stops it when your health drops.
  • Invalid input to math produces a report, never a silent failure or a corrupted value. A list that was never set is reported too. An empty list is not an error: foreach runs zero times.

Command reference

CommandWhat it does
send <text>send a command to the game
echo <text>message visible only to you
debug <text>message visible only in debug mode
play <sound>play a sound file
beepsystem alert sound
speak <text>say it out loud; a trigger's spoken alert cuts it off
flashflash Merlin's icon
pause <n>wait n seconds, plus any roundtime
delay <n>wait exactly n seconds
later <n> <line>in n seconds do one instant line; the script carries on
waitfor <words>wait for a game line containing the words
waitforre <pattern>pattern version of waitfor; keeps nothing
waiteval [n] <cond>wait until condition is true (n = timeout, seconds)
waitreply [n]wait until the game has answered this script's last command
move [n] <dir>send a step, wait for the room to change; $moved yes/no
nextroom [n]the same wait, for a step you sent yourself
walkto <room>the map walks you to a room by its id; stops the script if it cannot
set <name> <value>local variable
keep <name> <value>saved variable (Settings → Variables)
math <name> = <sum>numbers, $names, + - * / mod, ( ), round( ) abs( ) min( ) max( ) items( ) length( )
random <name> <lo> <hi>random integer, lo to hi, both included
timer start <name>start a timer; read $timer_<name>; timer clear <name>
text <name> shorten <x>drop the first word of x, keep the rest
text <name> first <x>keep only the first word
lastkeep only the last word
text <name> trim <x>take away extra spaces
text <name> upper <x>make every letter uppercase
lowermake every letter lowercase
text <name> replace "old" "new" <x>every old becomes new ("" deletes)
text <name> ordinal <n>2 becomes 2nd, 11 becomes 11th
text <name> opposite <d>north becomes south, ne becomes sw
turn on timer <name>turn on a Settings → Timers timer
turn off timer <name>turn it off
hold trigger group <name>keep a trigger group off while this script runs
release trigger group <name>turn the trigger group back on before the script ends
if <cond> then <line>conditional ( = != < > <= >= contains starts ends matches is set and or not in )
if <cond> { ... }several lines under one condition, with else { ... } and else if <cond> { ... }
<value> in <list>true if value ends with a list entry
repeat [n] ... until [words]loop n times, or until words appear, or with no number until told to stop
until eval <cond>…or until a condition holds
stopon <words>end the current loop if words appear
stopon eval <cond>…or if a condition holds
foreach <name> <list> ... endeachloop once per list item
list <name> add <item>put an item on a list
frontput it in front
removeremove the best-matching item
$list(2)second item in a list (the first item is 1)
$remain(<skill>)a game value that takes a name
Name: ... returnsection definition
gosub Namecall it
include <script>make a library's sections available to gosub
run <script> [values]start another script, handing it $arg1, $arg2 … and $args; a running one is left alone
args <name> <name> …name this script's arguments, so $name reads what $arg1 would
stopend this script
stop <script>end that script, if it is running
stop allend every script
match <sec> <words>declare a possible outcome
matchre <sec> <pattern>pattern version; captures → $arg1…
matchwait [n]wait for a declared outcome (bare = 30s timeout, stops; numbered = continues on timeout)
action <line> when <words>temporary trigger while the script runs (also: whenre, when eval)
action (<group>) on|off|clearmanage the script's own action groups

Patterns: | or · \d digit · + one or more · . any character (\. literal) · \[ and \] literal brackets · ( ) capture → $arg1… (in matchre, whenre, matches)

Glossary

  • script: A saved sequence of instructions, run top to bottom.
  • verb: The first word of a line; determines its action.
  • variable: A named value, read with $name.
  • list: A variable holding multiple values, separated by |.
  • condition: A test that is either true or false, such as $health > 50.
  • section: A named block within a script, run with gosub.
  • library: A script containing only sections, for include.
  • pattern: A regular expression, for matching variable text.
  • capture: A parenthesized group in a pattern, delivered as $arg1… by matchre, whenre, and matches.
  • action: A temporary trigger, a watcher that checks every game line while the script runs.
  • validation: The check before a script starts that reads its structure and reports every error it finds.

When a script stops with an error

A script that stops with an error reports two things in the Game panel:

  1. Where: the script's name and what went wrong, such as a waitfor that timed out after 30 seconds. When the message names a line, that's the line. When it doesn't, the last line of the trace below is where it stopped.
  2. How it got there: the last twenty lines it ran, oldest first, each with its line number.

Read the path that led to the error, not just the line where it stopped. A wait timing out inside the main loop means one thing; the same wait timing out inside a rescue section you didn't expect to be in means the problem started earlier. The Script Output panel shows a shorter trace: the last five lines that did something, and the sections the script passed through. Show the full trace opens all twenty. A script you stop yourself prints nothing. Neither does one that runs out of lines.

Example scripts

Short scripts to start from. Change the words to fit your game.

Eat and drink

send eat bread
waitreply
send drink canteen

You eat, Merlin waits for the game to answer, then you drink.

Treat your wounds

send lie
waitreply
repeat 30
send treat $firstname
pause 2
until Target is not injured
send stand

You lie down and treat your wounds until the game says there's nothing left to treat, then you stand up. It stops then, or after 30 tries, whichever comes first.

Practice on a target

send ready sword
waitreply
repeat
send attack target
pause 1
until I don't see

You ready your sword and attack the target over and over. When the target is gone, the game replies I don't see what you are referring to. That ends the loop and the script. To end it sooner, click Stop.

Walk a short path

move north
move north
move east

Each step waits for the next room before the next one goes. If a step is blocked, move gives up after 10 seconds and the next step goes anyway. To stop instead, add if $moved = no then stop after each step.

Greet a friend

send wave at $arg1
pause 1
send say Hello, $arg1!

Save it as Greet, then type .greet Marcus. You wave at Marcus and say Hello, Marcus! The name you type after the script's name is $arg1.

Wave to all your friends

set friends Marcus|Rose|Taath
foreach friend $friends
send wave at $friend
pause 1
endeach

You wave at each friend on the list, one at a time.

Rest until your fatigue is full

send sit
waitreply
repeat
pause 30
until eval $stamina >= 100
send stand

You sit and check every 30 seconds until your fatigue is full, then you stand up. It keeps checking as long as it takes.

Treat your wounds only if you're hurt

if $health < 80 {
  send sit
  waitreply
  send treat $firstname
}
else {
  echo You're fine.
}

If your health is under 80 percent, you sit down and treat your wounds. If not, Merlin tells you that you're fine.

Open a door

match Opened You open
match Opened already opened
match Locked It's locked
send open door
matchwait
Opened:
echo The door is open.
stop
Locked:
echo The door is locked.

You try the door, and the script tells you whether it opened or is locked. An open door counts as opened.

Get back up while you practice

action send stand when fall down
repeat
send attack target
pause 1
until I don't see

You attack the target over and over. If you're knocked down, you stand back up and keep going.

A reminder

delay 300
speak Time to eat.

Five minutes later, Merlin says Time to eat. out loud.

Do something different each time

random n 1 3
if $n = 1 then send smile
if $n = 2 then send wave
if $n = 3 then send bow

You smile, wave, or bow. Merlin picks one at random.

Count your sips

set sips 0
repeat 10
send drink canteen
math sips = $sips + 1
pause 1
until You're no longer thirsty
echo Sips: $sips

You drink until you're no longer thirsty, and Merlin tells you how many sips it took.

Look in all your bags

set n 1
repeat 3
text which ordinal $n
send open $which bag
waitreply
send look in $which bag
waitreply
math n = $n + 1
until

You open each of your three bags and look inside, from the 1st to the 3rd. text which ordinal $n turns 2 into 2nd, the way the game counts. Change the 3 to the number of bags you carry.