Zaytun Handbook

Language reference

Top level

include "items.zaytun"            # pull in another file — see "Splitting a game across files"

title: "Game Title"
start: roomId
intro: sceneId                    # narration shown before the first room — see "Scenes"
seed: 12345                       # pins the random stream — see "Randomness"
status { turns: false }           # optional; which STATUS lines to hide — see "Status"
flag flagName = true              # or a number (including negative), or a string — see "Variables"
flag chests[8]                    # eight slots under one name — see "Slots on a flag"
flag order[size(quiz)]            # ...as many as something else has, and it cannot drift
flag chests[8] = false            # ...with a starting value, if it matters
stat hunger { max: 100 start: 100 }    # a custom player stat — see "Custom stats"
stat hunger { max: 100 label: "Appetite" }   # ...that STATUS shows, under that word

player { hp: 20  maxhp: 20 ... }  # hp, attack, defense, score — see "The player's own numbers"
on death { ...statements... }     # what happens when they run out — see "The player's own numbers"

question qId "Name" { ... }       # a place where the game is asking — see "Questions"
scene sceneId { "paragraph" ... } # see "Scenes"
verb pray { ...statements... }    # a command of your own — see "Verbs"
proc name { ...statements... }    # a reusable block — see "Procedures"
proc name(a, b) { ... }           # ...that takes arguments — see "Procedures"
on turn { ...statements... }      # runs after every turn — see "Time"
group stock { sword, lamp }       # an order over things already declared — see "Arrays"
vehicle car "The Car" { ... }     # a thing a room stands in and the player rides — see "Vehicles"

npc npcId "Display Name" { ... }       # declared here, placed by whichever room they start in
npc npcId[3] "Name" { ... }            # ...three of them, addressed npcId[i] — see "People"
enemy enemyId "Display Name" { ... }   # a bestiary entry — see "Enemies"
enemy enemyId[8] "Name" { ... }        # ...eight of them, addressed enemyId[i] — see "A series of creatures"

local <declaration>               # a prefix, on room / item / npc / enemy / scene /
                                  # proc / flag / stat: the name is visible only inside
                                  # this file — see "Keeping a name inside its file"

room roomId "Display Name" {
  description: "text"             # any expression — see below
  on turn { ...statements... }    # every turn the player is HERE — see "Time"
  description when cond: "text"   # see "Conditional descriptions"
  dark if cond                    # see "Darkness"
  exit north -> otherRoomId
  exit south -> lockedRoomId if hasLantern else "It's too dark to go that way."
  exit down -> { ...statements... }   # runs instead of leading — see "Exits"
  item someItemId                 # places an already-declared item here at start
  vehicle car "The Car" { ... }   # ...and declares the vehicle standing here — see "Vehicles"
  npc guard "The Guard" {
    aliases: "guard", "sentry"          # other names TALK and ASK will accept
    examine: "text shown on 'examine'"  # any expression — see "People"
    dialogue: "Halt! Who goes there?"   # any expression — see "People"
    on talk { ...statements... }        # runs after the dialogue is printed
    topic "gate" { ...statements... }   # reached by ASK GUARD ABOUT GATE
    otherwise { ...statements... }      # ...and anything else they're asked
  }
  on enter {
    ...statements, run every time the player arrives here...
  }
  on exit { ...statements, run as the player leaves — see "On exit"... }
}

item itemId "Display Name" {     # the title is any expression — see below
  aliases: "alt name", "another"
  examine: "text shown on 'examine'"
  fixed: true                     # can't be picked up; 'take' always fails
  open: true                      # starts open (default false); declaring it, a lock, or
                                  # either handler makes the thing openable at all
  text: "what 'read' prints"      # any expression; makes the item readable
  contains: keyId, noteId         # makes it a container — see "Containers"
  opens if has(crowbar) else "It will not shift."     # a lock
  takes if <cond> else "..."      # whether TAKE may pick it up   — see "Whether it may
  drops if <cond> else "..."      # ...whether DROP may put it down    be taken, dropped, worn"
  equips if <cond> else "..."     # ...whether EQUIP may put it on
  unequips if <cond> else "..."   # ...whether UNEQUIP may take it off
  on take  { ...statements... }   # after a successful take, or instead of it
                                   # if fixed — see "On take"
  on drop  { ...statements... }   # after a successful drop — see "On drop"
  on use   { ...statements... }   # 'use' does nothing without this block — see "On use"
  on open  { ...statements... }   # runs after a successful open
  on close { ...statements... }
  on read  { ...statements... }   # runs after the text is printed
  on search{ ...statements... }   # makes the item searchable — see "Searching"
  on turn  { ...statements... }   # every turn, while in the room or carried — see "Time"
  on equip { ...statements... }   # weapons and armor only — see "On equip and on unequip"
  on unequip { ...statements... }
  count: 12                       # several of a thing — see "Several of a thing"
  plural: "arrows"                # optional; worked out from the title if you leave it out
  weapon: true  damage: 3         # see "Weapons and armor"
}

Every field is an expression

A room's title and a creature's hit points have always been expressions rather than literals. An item's fields are too, so any of them can come from a flag or from asking about something else:

flag lit = false
flag stock = 12

item lamp if lit then "a lit lamp" else "a dark lamp" { aliases: "lamp" }
item arrow "an arrow" { count: stock  plural: "arrows" }

When each one is worked out is the part worth knowing, and it follows what the field is for:

fieldworked out
the title, plural:, the else message on opens ifevery time it is shown
damage:, defense:every time they are used
count:, aliases:once, when the world is built

A title is worked out where it is shown, so lamp above reads as "a dark lamp" until something sets lit and as "a lit lamp" afterwards — with no set anywhere near the item. That is the same rule a room's and a person's title follow. A creature's title is different: it is frozen when the creature is spawned, because each spawned creature is its own instance of one description.

count: and aliases: are starting values. A quantity that re-rolled every time it was read would not be a quantity, and a vocabulary that shifted under the player would not be one either. A count: that comes out negative is clamped to none, the way a subscript outside a group reaches nothing rather than stopping the story.

aliases: is an expression on a person and a creature too, and worked out at the same moment, so a name the game decides can still be a name the player types:

flag rank = "sergeant"

npc boss "the " + rank { aliases: rank }
enemy raider[4] rname[member] { aliases: rname[member]  hp: 14  attack: 5  defense: 0 }

It was the one field a generated character could not have. Their titles have always been expressions, so a creature named from a flag could be called anything and answered to nothing — the vocabulary was fixed when the game was compiled and the name was not. A verb's aliases are still literals, and have to be: they are words the parser matches before any game state exists.

Comments

# a line comment
// also a line comment

Variables

A name that holds a value comes in five kinds. They differ in how long the value lives and in how much of the game can see it, and in nothing else:

how long it liveswho can see it
flag name = vthe whole game, and the save fileevery file
local flag name = vthe samethe file it is written in — see A flag that belongs to one file
stat name { max: n }the same, clamped to [0, max]every file, or one with local — see Custom stats
let name = vthe block it is written in, and no longerthat block and the blocks inside it — see Local values
a proc's parameterone callthat proc's body — see Procedures

A flag may hold several values under one name — flag chests[8] — which changes neither its life nor its reach; see Slots on a flag.

On top of those the engine keeps names of its own — hp, turn, noun and the rest. They read in any expression, some of them can be written, and nothing else in the game may take one of those names. They are gathered at the end of this chapter. Items and creatures carry numbers of their own too, reached through a dot: see A thing's own numbers.

All of them are written with set and read by naming them:

set lamp_lit = true
set chamber_kind[3] = 2
set hunger = hunger - 1
set maxhp = maxhp + 4
set rusty_knife.damage = 100

There is no separate statement for awarding a point, spending an arrow or lighting a lamp. One spelling writes everything a game can write, which is why the list above is short enough to hold in your head.

Declaring a flag

flag lamp_lit = false             # true or false
flag purse = 30                   # a number, which may be negative
flag last_words = "nothing yet"   # a string

A plain flag must be given a starting value, because that value is the declaration: flag lamp_lit = false is the sentence "the lamp starts unlit". Leaving it off is a compile error, and so is declaring a flag anywhere but the top level.

What it starts as does not lock it. A flag holds whatever was last written to it, so set purse = "broke" is accepted on a flag that began at 30, and nothing warns. It matters because + then joins instead of adding — purse + 1 reads as broke1, not 31. Declare a flag as the kind of thing it will hold and go on writing that kind. The one number the language does police is a stat, which clamps every write to [0, max].

Flags are global, and there is deliberately nowhere else to put one. A flag is exactly the name that has to outlive the block that set it; for one that does not, let is cheaper and cannot be read from the far side of the game by accident.

Every flag is saved and restored. A game that adds one keeps the save files it already had.

A flag that belongs to one file

A flag is normally visible to the whole game, which is the point of one. What that costs is short names: two areas both want a step counter and you end up writing archive_step and church_step by hand. local in front of the declaration gives the name to its file instead:

# archive.zaytun
local flag step = 0

# church.zaytun
local flag step = 0

Those are two different flags. Each file's set step = step + 1 moves its own, and a third file that declares no step of its own cannot read either — it gets refers to undeclared flag, the same complaint as a name that was never declared anywhere, because from that file's side there is no difference. local goes on stat as well, and on most other declarations besides; the full rule, and what it means for a room or an item, is under Keeping a name inside its file.

A local flag is still a flag in every other way. It is saved and restored, a let of the same name shadows it exactly as it shadows any other, and the engine's own names stay out of reach — local flag hp is refused for the same reason flag hp is.

One thing to know before renaming a source file. A local name is tagged with the file's, so step in archive.zaytun is written into the save file as step@archive. Rename that file and the tag changes with it: an old save still loads, and says so, but its step@archive now matches nothing and the flag comes up at the value it starts a new game with. Nothing warns, because nothing can tell that apart from a save written before the flag existed. It costs nothing while a game is yours to rebuild; it is worth knowing once players have save files.

The player's own numbers

player {
  hp: 20
  maxhp: 20
  attack: 4
  defense: 1
  score: 0          # optional — see "Score"
  maxscore: 1000
}

on death {
  finish "You collapse in the dirt. GAME OVER."   # optional — without this
}                                                  # block, death defaults
                                                     # to finish "You have died."

The player has numbers of their own, and this is where they are declared. hp, maxhp, attack, defense, score and maxscore are the engine's names for them: readable in any expression, and none of them available to a flag, a stat, a local or a parameter. attack and defense read as the effective values, equipment included, which is why neither can be written — see The engine's own names.

What the player fights is declared separately — see Enemies — and so is what they fight with, since a weapon or a shield is an ordinary item: see Weapons and armor for declaring one and Putting things in hands: equip and unequip for getting it into the player's hands, both under Items. alive() and ehp() ask the same kind of question about a creature that hp asks about the player.

hp and maxhp can also be assigned, which is how permanent upgrades work — a heart container is just an item whose on take raises the ceiling:

on take {
  print "*** Maximum health +4. ***"
  set maxhp = maxhp + 4
  heal 4
  remove heart_container from inventory
}

attack and defense are read-only (assigning them is a compile error). They read as effective values including equipment, so writing them back would quietly fold the gear bonus into the base — change them by handing the player a better weapon or shield instead.

A player with 0 effective attack cannot fight at all. Start the player at attack: 0 and bare hands do nothing to anything, which is what makes "you need a sword before you go in there" a rule rather than a suggestion. Give the player any positive base attack and this never comes up.

New statements: heal expr, hurt expr — adjust the player's hp directly (for traps, potions, scripted damage). Neither one prints anything on its own; pair with print.

Write one of hp: and maxhp: and the other follows it, the way a stat with a max: and no start: begins full. Write neither and the player has the twenty a game with no player block at all gets:

player { hp: 60 }        # 60 of 60
player { maxhp: 90 }     # 90 of 90
player { hp: 30  maxhp: 40 }   # starts already hurt, which is a thing a game may want
player { }               # 20 of 20

Starting above your own maximum is a compile error — player { hp: 200 maxhp: 80 } — for the same reason a score above its target is: health is held to its maximum everywhere, so the first heal, hurt or set hp would snap it down and the number the author asked for could never be true. The players hold a file down to its maximum as well, since a .zaytunb is not always one this compiler wrote.

Custom stats

The player has hp, attack, defense and optionally score. A game that wants more — appetite, warmth, air, sanity — declares them:

stat hunger { max: 100  start: 100 }
stat thirst { max: 50 }              # no start: means it begins full
stat depth  { start: 3 }             # no max: means no ceiling at all
stat tally  { }                      # neither: starts at 0, and counts as far as you like

A stat has a ceiling only if you write one. The four shapes:

writtenstarts atstops at
max: and start:the startthe max
max: alonethe max — it begins fullthe max
start: alonethe startnothing
neither0nothing

A stat that declares a maximum also declares two readable names, exactly the shape hp/maxhp has: hunger for the value and maxhunger for its ceiling. Both read in any expression, and set writes either:

on turn { set hunger = hunger - 1 }

verb diagnose {
  free: true
  print "Health " + hp + "/" + maxhp + ", appetite " + hunger + "/" + maxhunger + "."
}

verb eat { print "You eat."  set hunger = hunger + 40 }

What this adds over a flag is the floor and the ceiling. A flag holding the same number goes negative on the way down and past the top on the way up, and every site that touches it has to clamp by hand:

flag hunger = 100
set hunger = hunger - 30     # after four turns: -20

A stat clamps to [0, max] on every write, so the same four turns leave it at 0 and eat cannot push it past 100.

The floor is there whether or not the ceiling is. Not going below zero is the half of the clamp that needs no maximum to make sense, and it is what an uncapped stat still has over a flag: set tally = tally - 99 on a tally of 0 leaves 0, where a flag would go to -99.

A stat with no ceiling has no max name. maxdepth on a stat depth { start: 3 } is a compile error rather than a 0, because if depth >= maxdepth would otherwise be a test that is always true about a limit that does not exist. Give it a max: if it should have one. Lowering the ceiling pulls the value down with it; raising it leaves the value where it is — the same way maxhp already behaves.

A stat appears in STATUS when you give it a label:, and not otherwise:

stat hunger { max: 100  start: 100  label: "Appetite" }

The label is the player's word for it, which is why it is also the request to show it: hunger is appetite in one game and rations in another, and the name in your source is a name for the program. A stat without one is yours alone — print it in a verb, as above, and see Status.

Two names per stat means more ways to collide, and all of them are compile errors rather than a write landing somewhere unintended: with a built-in (stat hp, stat maxhp), with a flag of the same name, with another stat, or with another stat's derived name (hunger and maxhunger declared separately). A max of zero or less, and a start above the max, are errors too.

Local values

let x = <expr> binds a name for the rest of the block it is written in:

verb appraise {
  let worth = count(coin) * 3
  if worth > 10 { let bonus = worth - 10  print "Worth " + worth + ", " + bonus + " over." }
  print "You reckon " + worth + "."
}

It is gone when the block ends, so the same name may be used again in another one. A let in a nested block shadows one outside it, and a let shadows a flag of the same name for as long as it is in scope — the flag itself is untouched, and a proc the block calls sees the flag, not the local. That is the boundary a proc's parameters already have.

set x = ... writes wherever the name is bound: the local if there is one, then a parameter, then the flag. The engine's own names (hp, turn, score and the rest) cannot be shadowed, and the same name twice in one block is refused — the second would silently replace the first, and nothing would say which one a later line meant.

The engine's own names

These are the names the engine keeps. Each is documented where it belongs; the point of the table is to have the whole set in one place, since none of them may be used as a flag, a stat, a local or a parameter — that is a compile error, not a shadowing.

Readable and writable with set:

hp / maxhpthe player's health and its ceiling — see The player's own numbers
score / maxscorethe running total, if the game keeps one — see Score
<stat> / max<stat>each stat you declare, both halves — see Custom stats

Read-only (assigning one is a compile error):

attack / defensethe player's effective numbers, gear included — see The player's own numbers
turnhow many turns have passed — see Time
elapsedwhole seconds of the player's own time since the game began, and reset elapsed puts it back — see The clock on the wall
playerroomthe room the player is in — see Rooms
playerregion...and the region that room belongs to, or "" — see Regions
carried / carried_kindshow much is in the pack — see How much the player is carrying, under Items
noun / other / prepwhat the player typed, inside a verb — see Verbs
attackerinside on attack: "player", or the creature's id — see Who is striking: attacker
replyinside on answer: the line the player typed — see Questions
memberinside a series body: which member this is — see A series of creatures

attack and defense are the one pair whose read-only-ness is worth a sentence: they read as the value after equipment is added, so writing one back would quietly fold the sword's bonus into the player's own arm and count it twice at the next equip. Change them by changing what is in the player's hands.

attacker, noun, other and prep only mean something inside the block that gives them one — an on attack, a verb. They are readable anywhere, but outside such a block there is nothing for them to say and they read as the empty string, so an if noun == ... written in on enter is a test that never fires. Two are refused outright instead: member outside a series body, where its value would be a subscript into nothing, and reply outside a question's on answer, where there is no line to have been typed.

Rooms

A room is a place the player can be, and everything else in the game lives in one. start: names the room the game opens in.

start: cell

room cell "A Cell" {
  description: "Four walls of damp brick. A door, north."
  exit north -> corridor
}

The display name is an expression, not just a string, so a room can name itself from game state — which is what makes a generated floor possible:

room c3 c3_name { ... }        # c3_name is set by whatever generated the floor

description: is an expression too. For a room whose description depends on state, prefer description when over building one long conditional string — see below.

Exits

exit north -> corridor
exit south -> vault if hasLantern else "It is too dark to go that way."
exit south -> vault if hasLantern
exit south -> vault if hasLantern else { ...statements... }
exit down  -> { ...statements... }

An exit's name is not restricted to a compass direction. exit boat -> ..., exit through -> ... and exit in -> ... all work, and the player reaches them by typing the name on its own — see Naming an exit moves you.

if <cond> makes a locked exit. The condition is re-checked every time the player tries it, so an exit unlocks itself the moment the state changes; there is nothing to remember to update. The else is optional, and without one a blocked exit says "That way is blocked."

What the else may be. A string, any expression, or a block of statements:

exit north -> vault if open else "Locked."
exit north -> vault if open else "You need " + (3 - keys) + " more keys."
exit north -> vault if open else {
  set rattles = rattles + 1
  if rattles == 1 { print "You rattle the door. Something stops moving." }
  else { finish "The door opens from the other side. THE END." }
}

The block is what makes a refusal a consequence rather than a line of text: it can count the attempts, set a flag, move the player, end the story. Before it existed the else took a string literal and nothing else, so "warn the first time, end it the second" had to be written somewhere that was not the door — and a message could not so much as mention how many keys were missing.

A blocked exit still costs a turn — fumbling with a locked door takes a moment, which matters in a game with a deadline — and the turn is counted after the refusal runs, so on turn sees whatever the block did.

A way out that is not a way to a room. Put a block where the room goes and the direction runs it instead of leading anywhere:

exit north -> { finish "The mud doesn't hold your weight this time." }
exit down  -> { set falls = falls + 1  print "You slither back down." }

The player stays where they are unless the block moves them itself — goto inside it works, and is the way to make a direction lead somewhere only a script can work out. Everything a block can do it can do here: print, set, spawn, finish.

This is the door the player can open onto something that is not a place. Without it the same scene needs a room that is not a room — an empty title, an empty description, and an on enter whose only job is to end the story:

room mud "" { description: "" on enter { finish "The mud doesn't hold your weight." } }

A condition still applies, and reads the way it does anywhere else:

exit north -> { finish "You jump." } if brave else "You look down, and do not."

Opening and closing a way out from a script

A room can also gain and lose a passage it never declared:

open exit north -> vault        # in the room the player is in
open exit north -> room("c" + n)
close exit north

open exit takes any room, spelled out or built from a value, and replaces whatever that direction did before. close exit shuts a direction whether the room declared it or not — so a way out with no condition on it can still be barred, and a trapdoor a room never mentioned can be opened.

What the game did last is what the player sees: an opened direction is listed first, in the order the passages were opened, then whatever the room still declares. It is a way out like any other once it is there — an opened exit boat is walked by typing boat, exactly as a declared one is, and closing it takes the word away again. Both lists go into a save, so a map that changed shape comes back the shape it was — and the two players write the same save, so one written in a terminal opens in a browser with its passages intact.

Between this and a declared exit ... if ..., prefer the declaration where the set of directions is known: it is checked when the game is compiled, and a condition reads better than a pair of statements. Reach for open exit when the direction itself is not known in advance.

On enter

room outside "Outside" {
  description: "Grass. Weather. The rest of your life."
  on enter { finish "You are out. THE END." }
}

on enter runs before the room is described, so anything it prints appears above the room's name. That is usually what you want — an ambush, a trap, a scene that sets up the arrival.

An ending fired here replaces the description rather than preceding it. finish stops the turn where it stands — see Time — so nothing below it runs and the room's name and description are never printed:

> east
You are out. THE END.

Write whatever the player should read last into the finish itself. A description the block never reaches is a description nobody sees, so a final room usually has little to say in one.

A block that moves the player wins, in the same way. on enter { goto cellar } is how a trapdoor is written: the player walks in, the floor gives way, and the cellar describes itself. The arrival that ran the block is then over — the room they never really stood in does not describe itself underneath the one they ended up in, and a question reached that way is asked once. It is the same rule as on exit's, from the other side of the doorway.

It runs on every arrival, not only the first. Gate it on a flag if it should happen once.

On exit

room cell "A Cell" {
  description: "Four walls."
  exit north -> corridor
  on exit { print "The door swings shut behind you." }
}

It runs as the player leaves, before they arrive anywhere. The room being left is still the current one while the block runs, so anything that asks about this room — here(x), count_here(x), npchere(x), member_here(x) — still answers about the room being left, not the one being walked into. That is the point of the block: it is where you notice what the player is walking away from.

alive(x) and ehp(x) are not among them. They name one creature and answer from anywhere, in here exactly as everywhere else — see Enemies.

Leaving is leaving, however it happens: walking through an exit and goto both fire it. Two things are not a departure and do not: the game's opening move, which puts the player where they already are, and a goto to the room they are already in.

A block that moves the player wins. on exit { goto cell } sends them to the cell and the move they were making is abandoned — they asked to go north, something caught them, and carrying on would put them somewhere they were diverted from. One departure runs one block, however many rooms the block itself sends them through.

Moving the player: goto

goto shrine

goto puts the player in a room without their walking there, and the room is then described exactly as though they had walked in — including its on enter. It is how a scene, a trapdoor or a time machine delivers somebody somewhere.

Where the player is: playerroom

playerroom is the room the player is standing in, as its id:

verb whereami {
  free: true
  print "You are in " + playerroom
}

It is a string, because a room id already is one everywhere else — which means it composes with the room reference that takes a name worked out while the game runs:

move hound to room(playerroom)      # bring the creature to whoever is being hunted

It is read-only. goto is how the player moves; assigning to playerroom is refused rather than silently doing nothing.

Comparing it against a literal is checked. A room name reaching the compiler as a plain string is the one place a misspelling can be caught, and a misspelling here is otherwise silent — the branch simply never runs:

if playerroom == "kitchn" { ... }
→ zaytunc: no room named "kitchn" — "playerroom" reads a room id

A name worked out while the game runs (playerroom == "ward_" + n) is not checked, for the same reason item(<expr>) is not: it is a name the compiler never sees, and it is spelled differently on purpose.

This is what a game needs to give anything a position of its own — a creature that walks toward the player, a person who follows, a trap that fires only where somebody is standing. Without it the only way to know is to write the room down by hand in every on enter, and nothing checks that what was written is true.

For the larger area a room is part of — a forest, a keep, a quarter of a city — see Regions, and its playerregion.

Reading the ways out

A room's exits are declarations, and these four expressions are how a game reads them back:

exitcount(<room>)how many ways out that room has
exitname(<room>, i)the i-th one's direction, as a string; "" past the end
exitto(<room>, dir)the room it leads to, as its id; "" if there is no such way
exitopen(<room>, dir)can it be walked right now?
let i = 0
repeat exitcount(playerroom) {
  let d = exitname(playerroom, i)
  print d + " goes to " + exitto(playerroom, d)
  set i = i + 1
}

They enumerate rather than taking a fixed compass, because a direction is any word: exit boat -> jetty is an ordinary way out, and open exit can invent one the room never declared.

<room> is a room reference like goto's, and what may stand there is the list below.

What can stand where a room is named

Everywhere a room is named — the four reads above, regionof, roomtitle, the contents family, goto, move, spawn, an exit's target — the same short list is accepted:

kitchena name spelled out, and checked when the game is compiled
room(<expr>)a name worked out while the game runs
playerroomthe room the player is in
roomof(x)the room a thing, a person, a creature or a vehicle is in
exitto(<room>, dir)the room a way out leads to

The last three need no room(...) round them. room(<expr>) is deliberately visible — a name built out of text is a name the compiler never saw, and a reader should be able to tell at a glance which names were checked — but these three did not come out of text. The engine answered them, about a room it had already found, so handing that answer back to room(...) only says the same thing twice:

regionof(room(roomof(wolf)))        # ...was the only way to ask it
regionof(roomof(wolf))              # which part of the map the wolf is in

A room reference is that list and nothing else — not any expression that happens to read as text. regionof(roomof(wolf) + 1) is the syntax error it has always been, and room(<expr>) is how a name a game builds is written.

Nowhere is an answer. roomof(x) is "" for a thing that is in no room, and exitto is "" for a way out that leads nowhere nameable — so a question can be asked about nowhere, and nowhere has no region, no title, no ways out and nothing standing in it. Each read gives the empty answer of its own kind, quietly:

regionof(roomof(gem))               # "" — the gem is in your pocket
roomtitle(roomof(gem))              # ""
itemcount(roomof(gem))              # 0
exitopen(roomof(gem), "north")      # false

A name that is not empty and reaches no room is a misspelling, and says so out loud while the game runs. So does a command: goto roomof(gem) with the gem in your pocket reports, because a command that quietly does nothing is exactly the one worth being told about.

What counts as a way out. The list is what the player is shown, in the same order: passages opened while the game ran come first, in the order they were opened, then whatever the room still declares. A direction close exit has taken away is not there at all.

A condition does not hide a way out — it shuts a door:

close exit north                                    # the room no longer has a north passage
exit north -> hall if has(key) else "Locked."       # it has one; you cannot walk it yet

exitcount and exitname describe the map, so both a locked door and an open one are in the list; exitopen is the one that asks whether the player could pass. The distinction matters as soon as anything else moves: a ward door locked to the player is not a wall to the things already inside.

Two answers worth knowing:

Between them, these are what a game needs to walk its own map: a creature that paths toward the player, a door that reports where it goes, a map command that draws what the player has seen.

Reading what is in a room

The exits are one half of what a room holds; these six are the other. They enumerate for the same reason, and the name half answers an id, which is what item(<expr>), npc(<expr>) and enemy(<expr>) take — so walking a room and acting on what is in it composes out of forms that already exist:

itemcount(<room>)how many things are lying there
itemname(<room>, i)the i-th one's id, as a string; "" past the end
npccount(<room>) / npcname(<room>, i)the people standing there
enemycount(<room>) / enemyname(<room>, i)the creatures standing there, alive
verb rout {
  let i = 0
  repeat enemycount(playerroom) {
    let who = enemyname(playerroom, i)
    set ehp(enemy(who)) = ehp(enemy(who)) - 5
    set i = i + 1
  }
}

Before these, a room's contents could be asked about one name at a time — here(lamp), npchere(guard), goblin is here. A game could ask is the lamp here and never what is here, so anything that had to act on a roomful kept its own list of what it had put there, by hand, and the two could drift apart with nothing to say so.

Three rules, each following one that already existed:

The order is the order the room lists them, which is the order look prints them in.

What a place is called: roomtitle

Everything above answers with an id. roomtitle(<room>) answers with the name the player is shown at the top of the room, worked out now:

roomtitle(kitchen)                  # "The Kitchen"
roomtitle(playerroom)               # ...where they are standing
roomtitle(room("sea" + y + x))      # ...a name worked out while the game runs
roomtitle(exitto(playerroom, "north"))   # ...the room through the north door
roomtitle(roomof(hound))            # ...wherever the hound is

It takes a place, which is what goto takes: a room or a question. Both are shown under a name and both have one to give back. A vehicle is not one — it is a thing standing in a room, and the room is the place.

A title is an expression, so this is worked out at the moment it is asked and follows whatever the game has done to it — a room named from a flag answers with what the flag says now, exactly as the room's own heading does.

Without it, a game that wanted to say where something was — on a drawn map, in a table of routes, in "when it clears you are in the Coffin Bank" — had to write every title down a second time in a flag table and keep the two in step by hand. The second copy is the bug: it is right on the day it is written and wrong the first time a room is renamed.

Where anything else is: roomof

playerroom answers for the player. roomof(x) answers for everything else — an item, a person or a creature — and reads the same way, as a room id:

roomof(hound)                      # a creature
roomof(guard)                      # a person
roomof(lantern)                    # an item, if it is lying in a room
roomof(patient[3])                 # one member of a series
roomof(enemy("ghoul" + n))         # a name worked out while the game runs

One spelling for all three kinds, for the reason X is here has one: an author should not have to know which kind of thing they are asking about before they can ask where it is. The rules are is here's rules, so the two can never disagree:

if roomof(hound) == playerroom { print "It is in here with you." }

"" is the answer for anything that is in no room: a carried item, a dead creature, a series member that was never spawned. It is falsy and prints as nothing, so roomof(x) == playerroom is false rather than accidentally true when the thing is nowhere at all.

It reads a room id, so it stands where a room is named — regionof(roomof(hound)), roomtitle(roomof(hound)), itemcount(roomof(hound)), goto roomof(hound). See What can stand where a room is named, which is also where nowhere's own answers are.

Comparing it against a literal is checked exactly as playerroom is:

if roomof(hound) == "kitchn" { ... }
→ zaytunc: no room named "kitchn" — "roomof" reads a room id

"" is the one literal that names no room and is still the right question — roomof(x) == "" asks whether the thing is nowhere, and is accepted for the reason playerregion == "" is.

It is read-only. move is how a person or a creature travels; assigning to roomof is refused.

roomof walks the rooms to find the thing, where playerroom reads a single value, so it costs a little more — enough to be worth knowing if a game calls it for every creature on a very large map, and not enough to think about otherwise.

Conditional descriptions

A room can carry any number of description when <cond>: lines alongside its plain description:. They're checked in declaration order at look time, the first one whose condition holds is printed, and the plain description: is the fallback when none do:

room hall "Great Hall" {
  description when torches >= 3: "Three torches blaze; every corner is lit."
  description when torches == 1: "A single torch gutters against the dark."
  description when isopen(gate): "The hall is dark, but the gate stands open."
  description: "The hall is pitch dark."
}

Conditions are ordinary expressions, so this covers both "a variable reached a value" and "an event happened" — set a flag from an on block and test it here. The inline if cond then a else b expression still works on a plain description: too; the when form is the readable option once you have more than two cases.

A vehicle carries them too, in the same shape and read the same way — what it is like in here with the lamp lit, or once she has been hulled. See Vehicles.

Darkness

dark if <cond> makes a room's own items, NPCs, and enemies unknowable while the condition holds — not listed by look, and not valid targets for take, examine, open, use, talk, attack, or anything else that would require seeing them:

room crypt "Burial Crypt" {
  dark if not (has(lamp) and lit)
  description: "Old bones line the walls. A stone sarcophagus sits at the center."
  description when not (has(lamp) and lit):
    "Absolute dark. Cold air moves past your face — from where, you can't tell."
  item sarcophagus
}

Two things stay unaffected by darkness, deliberately:

Nothing else about the room is affected either: on enter — ambushes included — still runs normally, since a hostile enemy doesn't need you to see it to attack you. Pair dark if with description when (as above) to say what the room feels like blind; dark if is what makes that blindness real rather than just descriptive.

Questions

A question is a place where the game is asking. It is declared like a room, entered like a room, described like a room, and left like a room — and while the player is in one, every line they type is an answer rather than a command.

question sphinx_riddle "The Sphinx" {
  description: "The sphinx does not move. \"What walks on four legs at dawn,
                two at noon, and three at dusk?\""

  on answer {
    if reply == "man" or reply == "a man" or reply == "human" {
      print "The sphinx folds its wings and stands aside."
      goto north_road
    }
    else if reply == "dog" or reply == "cat" {
      print "\"An animal. You are not listening.\""
      goto sphinx_riddle                     # wrong: it asks again
    }
    else { print "\"No,\" says the sphinx."  goto sphinx_riddle }
  }
}

room gate "The Gate" {
  description: "A road, and something sitting across it."
  exit north -> sphinx_riddle            # a door can open onto a question
}

> north

The Sphinx
The sphinx does not move. "What walks on four legs at dawn, two at noon, and three at dusk?"
> man
The sphinx folds its wings and stands aside.

The North Road
...

The player types the answer. That is the whole point: without this, a game that asks a question has to invent a verb — ANSWER MAN, SAY MAN — and then teach the player to use it.

What a question has, and what it does not

description:the question itself, printed on arrival and again on look
on enteras a room's: runs on arrival, before the description
on answerruns on each line the player types, with reply bound to it
on exitas a room's: runs as the player leaves, whatever moved them

A question deliberately has none of a room's contents, and each is refused by name:

why
exitevery line typed there is an answer, so north is an answer and not a direction. The way out is goto, inside on answer
items, people, creaturesnothing is there. A question is a place made of one sentence
on turnno time passes in a question, so there would be no turns in it to run
darka question is not a room and cannot be unlit

reply

reply is the line the player typed, trimmed. It is an engine name like noun and attacker: read-only, and readable only inside an on answer — anywhere else it is refused, the way member is refused outside a series.

Everything the language has for strings and numbers works on it:

question guard_name "The Gate" {
  description: "\"And what do they call you?\""
  on answer {
    if length(reply) == 0 { print "\"Nothing at all? Suit yourself.\"" }
    else { set player_name = reply  print "\"" + reply + ". I'll remember it.\"" }
    goto yard
  }
}

question keypad "The Keypad" {
  description: "Four digits, and a very small screen."
  on answer {
    if not isnumber(reply) { print "It wants numbers." }
    else if tonumber(reply) == 4021 { print "The bolt withdraws."  goto vault }
    else { print "A flat tone, and nothing." }
  }
}

isnumber() and tonumber() take noun, other or reply — the three things that are what the player typed, and nothing else.

Going to a question is a call

goto into a question is not travel. It is a call, and it has two halves:

It stops the block it was written in. Nothing after it runs, because the player has not answered yet:

verb hire {
  print "You put your head round the door."
  goto take_job
  print "this never runs"
}

And when the question has been answered, the player is handed back to where they were asked — unless the handler sent them somewhere itself, in which case the handler wins:

question take_job "The foreman looks you over." {
  description: "Long hours, and it starts Monday. Well?  YES or NO"
  on answer {
    if lower(reply) == "yes" { set employed = true  goto the_yard }   # handler decides
    else { print "He shrugs." }                                        # back to where asked
  }
}

That is the whole rule, and it is what lets a question be asked from the middle of a verb without either end doing any bookkeeping.

A question asked from inside another question's handler comes back to the last real room, not to the question that asked it.

Leaving, and staying

goto is how the handler decides where the player goes, and it does not care which kind of place it names:

on answer {
  if reply == "left" { goto west_tunnel }         # ...a room
  else if reply == "right" { goto east_tunnel }
  else { goto are_you_sure }                      # ...or another question
}

An on answer that moves nobody hands the player back to where they were asked, by the rule above — including when they walked in through an exit, in which case they are put back in the room they walked out of.

So to keep the player in the question, say so: goto to the question itself. That is how a sphinx that asks until it is answered is written, and how "wrong, try again" is written, and it reads as what it is — ask that one again. look reprints the question for anyone still standing in it:

on answer {
  if right(reply) { print "Yes."  do next_drill }
  else if tries == 0 { set tries = 1  print "Not quite."  goto drill_fold }
  else { print "The answer is " + model  do next_drill }
}

The test is whether the handler did a goto, not whether the room changed. Those look the same and are not: asking the same question again changes no room, and a handler that means stay must not be read as a handler that meant nothing.

The other side of that: a handler that always sends the player back to the question is a place they cannot leave. Sometimes that is exactly right, and no compiler can tell it from a mistake — the way out may be behind a flag that becomes true later. What is refused is a question with no on answer at all, which could never read what the player types and so could never be anything but a trap.

What still works while a question is waiting

Six words, and no more:

look / lreprints the question. Someone who missed a line has to be able to hear it again
helpsays what to do
save, restore / loada player who cannot save until they solve a riddle loses their evening
quitthere is always a way out of a program

Everything else is the answer — north, take lamp, again, status, and any word a game cares to use. Each exempt word is a word a game can never use as an answer, which is why the list is this short. Nor is a line cut on . or then while a question waits: 4.5 and left. then right are answers, and the answer is the whole line.

No turn passes. turn does not advance and no on turn fires, so a creature does not get a free swing while the player is thinking. That holds for look in a question too.

A quiz: a group of questions

Questions are the one kind of place that can be grouped, because a quiz is a set that gets iterated rather than a map that gets walked:

question q_capital "Question" { description: "Capital of Iceland?"  on answer { ... goto q_river } }
question q_river   "Question" { description: "River through Baghdad?" on answer { ... goto results } }

group quiz { q_capital, q_river }

That gives size(quiz), quiz[i], and a compile-time guarantee that every name in the braces exists — so a misspelled question is caught before anybody plays, and the count cannot drift from the declaration. goto quiz[i] sends the player to one.

verb begin { free: true  goto quiz[0] }
verb pick  { free: true  goto quiz[random(0, size(quiz) - 1)] }

question(<expr>) reaches one by a name worked out while the game runs, the way room(<expr>) and item(<expr>) do — for when the names really are numbered:

goto question("q" + random(1, 20))

A game that should differ on each replay must not declare seed:. A seed pins the random stream, which is what it is for; without one, the clock picks it — see Randomness.

Regions

A room is where the player is. A region is the larger area several rooms belong to — a forest, a keep, a quarter of a city — and it is a name over rooms declared in full elsewhere, the way a group is a name over items:

region mirkwood { forest_entrance, old_forest, river_bank }

room forest_entrance "The eaves of Mirkwood" {
  description: "The road runs in under the trees."
  exit north -> old_forest
}

That declares nothing of its own. forest_entrance is still an ordinary room with its own description, its own exits and its own on enter; the region says only that those three rooms are one area.

The player is never in mirkwood. They are in river_bank, which is part of it. A region has no description, nothing is spawned in one, nothing stands in one, and goto will not take one — it is not a place but a fact about places, and everything that reads it reads it through the room the player is standing in.

A region names rooms on the world map. It never names a room inside a vehicle, because a vehicle moves and a region does not — a room of hers is in the region she is in, which changes as she is driven. See Vehicles.

Regions are optional. A game that declares none behaves exactly as it did before they existed, which is what every game written so far does.

Which region the player is in: playerregion

playerregion is the region the room the player is standing in belongs to, as its id, and "" when that room belongs to none:

room river_bank "The river bank" {
  description: "Dark water, moving fast."
  on enter { if playerregion == "mirkwood" { print "The forest presses close." } }
}

It is a string, the way playerroom is one, and it follows the room: walking from forest_entrance to old_forest leaves it mirkwood, walking out of the wood changes it, and walking into a room no region names empties it. It is read-only — goto is what moves the player, and the region follows.

set playerregion = "moria"
→ zaytunc: "playerregion" is read-only — it follows the room the player is in

Comparing it against a literal is checked, for the same reason playerroom is: a misspelt region is otherwise silent, and the branch simply never runs.

if playerregion == "mirkwod" { ... }
→ zaytunc: no region named "mirkwod" — "playerregion" reads a region id

playerregion == "" is the exception, because that is how a game asks whether the room it is in belongs to no region at all.

Any room's region: regionof

regionof(<room>) asks the same question about a room the player is not standing in, and takes a room the way exitcount and the rest do — see What can stand where a room is named:

regionof(river_bank)                 # "mirkwood"
regionof(playerroom)                 # the same answer `playerregion` gives
regionof(room("cell" + i))           # ...and for a name worked out while the game runs
regionof(roomof(wolf))               # ...whichever part of the map the wolf is in
regionof(gatehouse)                  # "" — that room is in no region

It is what a creature with somewhere to be needs. roomof(x) says which room a thing is in, and this turns that into which part of the map it is in:

enemy wolf "a grey wolf" { hp: 12 }

verb listen {
  free: true
  if regionof(roomof(wolf)) == playerregion { print "Something is moving, not far off." }
  else { print "The wood is quiet." }
}

roomof answers a room id, and a room id is what regionof takes, so the two go together with nothing between them. A beaten wolf is in no room, and nowhere is in no region: the branch is false rather than an error, which is what lets this be written without a guard in front of it.

Without it that question has to be written as the area spelled out room by room — if roomof(wolf) == "forest_entrance" or roomof(wolf) == "old_forest" or ... — which is the hand-kept list a region exists to abolish, and which goes stale the moment a fourth room joins the wood.

A region's own blocks

A region has the same three blocks a room has, written after its rooms:

region mirkwood {
  forest_entrance, old_forest, river_bank

  on enter { print "The trees close over the path behind you."  set under_leaves = true }
  on exit  { print "You come out from under the trees."  set under_leaves = false }
  on turn  { if chance(10) { print "Something moves, further in." } }
}

They follow the region, not the room, and that is the whole difference between them and a room's own three:

on enterthe player has arrived in one of its rooms from outside the region
on exit...and is leaving it for a room the region does not hold
on turnevery turn the player spends in any of its rooms

So walking from forest_entrance to old_forest runs neither on enter nor on exit — the room changed and the area did not — while on turn runs on both of those turns and on none spent outside the wood. Walking from river_bank into another region runs the one being left and then the one being entered, in that order: out of the forest, and into the village. Walking into a room no region names runs the on exit and nothing else.

The region holding the start: room is entered when the game begins, exactly as that room's own on enter runs. Restoring a save is not an arrival: the player comes back where they were, so nothing fires, and the next move out of the region runs the region they were actually in.

The order against a room's own blocks is outermost first on the way in and innermost first on the way out — you walk into the forest and then into the clearing; you step out of the clearing and then out of the forest:

on turn     the game's `on turn`, then the region's, then the room's
arriving    the region's `on enter`, then the room's
leaving     the room's `on exit`, then the region's

A player who is riding is in the room the vehicle is standing in, so nothing here has a case for vehicles at all: driving a car out of the wood and into the village is the player moving out of one room and into another, and the region's blocks run because that is what happened. The one extra is the vehicle's own on arrive, which is the innermost of the three and runs last.

driving out of one region and into another
            the region being left's `on exit`, then the one being entered's `on enter`,
            then the vehicle's `on arrive`

A vehicle carries only the player inside it: move ferry to anchorage changes no region of theirs unless they are aboard the ferry. And a save taken afloat comes back afloat — the region is recorded without its on enter running, so the next passage out of it fires the right on exit and nothing fires on the way in.

A block that moves the player wins, and the move it interrupted is abandoned — the rule a room's on exit already follows. A region whose on exit says goto gatehouse is a wall: the player never reaches wherever they were going.

What the compiler checks

Every name in a region is written out, so all of it is checked before anybody plays:

region mirkwood { forest_entrance, rivendell }
→ zaytunc: refers to undeclared room "rivendell"

A room belongs to one region at most, because playerregion has one answer to give:

region mirkwood { river_bank }
region anduin   { river_bank }
→ zaytunc: room "river_bank" is already in region "mirkwood" — a room belongs to one region at most

and a room named twice inside one region, or a region declared twice, is refused the same way. A question may be named in a region as a room may, since a question is a place the player can be.

A region is reached by no name, so — unlike a group — its own name may be anything; it is never looked up, only compared as the string playerregion gives back. That is also why a region cannot be local: renaming it for a file would quietly stop that comparison matching.

What a region is for

Anything that is true of a whole area rather than of one room: what the weather is doing over it, whether the guards there have been roused, which music plays in it, whether the player has ever been in that part of the map at all.

flag roused = false

region keep { gatehouse, yard, hall, undercroft
  on enter { if roused { print "The bell is still ringing somewhere above." } }
  on turn  { if roused and chance(20) { print "Boots, on stone, a floor away." } }
}

Written without regions each of those is a flag the author sets by hand in every on enter of every room in the area, and keeps in step by hand for ever after — which is the kind of bookkeeping that is right on the day it is written and wrong three rooms later.

Vehicles

A room is where the player is. A vehicle is a thing standing in a room that the player can be inside — a car, a boat, an aeroplane, a horse. It is declared the way an item is, with four things added, and all four are about being inside it.

vehicle car "The Car" {
  description: "A cold vinyl seat and an ashtray full of somebody else's cigarettes."
  examine: "Rust along the sills."           # ...and what it looks like from outside
  aliases: "motor", "banger"                 # ...and other words that reach it
}

A vehicle may carry description when <cond>: lines as well, exactly as a room does — checked in declaration order when the player looks, first match wins, plain description: the fallback:

vehicle car "The Car" {
  description when swamped: "Water over the floorpan and the seats afloat."
  description when lit:     "Cold vinyl, and the dome light showing every crack in it."
  description: "Cold vinyl, and you cannot see the far door."
}

They are the inside view, which is what a description is. examine: is the outside one and wins over all of them; a vehicle with no examine: is examined with whichever of these is showing.

A vehicle is declared where it stands, which is the room it is standing in — the way a creature is declared in the room it is in:

room drive "The Drive" {
  description: "Gravel, and a car nobody has moved in a fortnight."
  item tyre
  vehicle car "The Car" {                    # ...and here it is, standing in it
    description: "A cold vinyl seat and an ashtray full of somebody else's cigarettes."
    aliases: "motor", "banger"
  }
  exit south -> garden                                            # on foot
  exit north -> village if aboard(car) else "It is four miles."   # ...and by car
}

> look
The Drive
Gravel, and a car nobody has moved in a fortnight.
You see here: a spare tyre.
Standing here: The Car.
Exits: south.

> examine car
Rust along the sills.

> enter car
INSIDE The Car
A cold vinyl seat and an ashtray full of somebody else's cigarettes.

OUTSIDE The Car
The Drive
Gravel, and a car nobody has moved in a fortnight.
You see here: a spare tyre.
Exits: south, north.

> north
INSIDE The Car
A cold vinyl seat and an ashtray full of somebody else's cigarettes.

OUTSIDE The Car
The Village
...

> out
The Village
...
Standing here: The Car.

The player is in two things at once, and that is the whole of it. They are in the car, and the car is in the drive — so playerroom is "drive" whether they are sitting in it or standing beside it, and playervehicle is the other half of the answer. Nothing that asks where the player is has to know about vehicles.

That is why LOOK from inside prints two descriptions: the thing they are in, and then the room it is standing in. They are in both — and each block is headed by what it is to the player:

INSIDE The Car       the thing the player is in
OUTSIDE The Car      ...and then the place it is standing in, naming itself
The Drive              exactly as it does to somebody on their own feet

Two bare headings with a blank line between them would leave the player to work out that the first is a car and the second is a drive, and nothing in the text says so: both read as places. The marking appears only while the player is aboard, so a room on foot is headed by its own plain name exactly as it always has been.

Getting in and out

ENTER, BOARD, EMBARK, MOUNT, RIDE, ABOARD, CLIMBwith a name. Typed bare it asks Enter what?, as every other verb that acts on a thing does
board car from a scriptfor when the game decides

and the ways out: DISEMBARK, DISMOUNT, ALIGHT, any of the vehicle's own disembark: words, and disembark from a script.

...and the way to take her somewhere: STEER <direction>, or any of her own steer: words with a direction after it. GO drives her too — sitting in a car, the room's ways out are hers, and walking one drives her — so STEER matters most for a hull with rooms, where the room the player is standing in has ways out of its own and one word cannot mean both. See Walking her, and sailing her.

Not OUT, and not LEAVE. Both of those are things a player says about a room as readily as about a boat, and a room may have a way out called out — five games in this corpus do. While the player sat in a car parked in such a room, the room printed Exits: out. and typing it got them out of the car. The room's exits are the author's to name, so the engine gave the word back: every way of getting out of a thing is now a word that is about things only.

A name is not a command. Typing car on its own does nothing, exactly as typing lamp does nothing: what a thing is called is not something to do with it. ENTER CAR is the command, and it takes the name the way every other command takes one. The disembark: words are the exception, and are not names: getting out takes no noun — there is only ever the one vehicle the player is in — so a bare word is the only way to spell it, and disembark: is how a game adds its own.

aliases: "motor", "banger"       # ...as an item has aliases: ENTER MOTOR gets you in
disembark: "ashore", "get off"   # ...and extra ways out, typed on their own
steer: "drive", "pull away"      # ...and extra words for taking her somewhere

A way out may be a phrase. get off, step down, climb out — the whole line is the word, because getting out takes no noun and there is nothing else the rest of it could be. A phrase whose first word is one of the engine's works too: GET is the engine's word for TAKE, and a way out the game declared is read before the built-ins, for the reason a verb of your own is — what the game declares comes before what the engine has. It is read only while the player is inside the vehicle that declared it, and a verb of your own still wins over both.

While they are aboard, a declared way out also wins over a room exit of the same name: the thing they are sitting in is nearer than the room it is standing in. A room with exit ashore and a ship with disembark: "ashore" is one word declared twice, and while aboard the ship has it.

ENTER <name> reaches the vehicle by its title, its title without the article, its id or any of its aliases: — the same four-way rule an item answers to, and there is no second list beside it: reaching a vehicle and getting into it are the same act. HELP offers enter <thing> and disembark only in a game that declares a vehicle.

The name is not optional. ENTER on its own answers Enter what? and waits, the way TAKE, EXAMINE, OPEN and GO do — it is the Standing here: line that says what is here to get into. It used to take the one vehicle in the room when the line was bare, which made it the only verb in the language that guessed: defensible, because a room usually holds one vehicle where it holds many takeable things, and still a rule a player met by accident. DISEMBARK is the one that needs no name, and always was, because there is only ever one thing you are inside.

There is no EXIT. That word leaves the program, above the story entirely, so a command of that name would never run — which is the same reason a game may not declare verb exit. And DISEMBARK typed by somebody who is not sitting in anything says so — You aren't in anything to get out of — rather than looking for a room to leave.

Getting in is not going anywhere. The player was in this room and they still are, so nothing about the room fires: no on exit, no on enter, no region. What runs is the vehicle's on enter, and then both places are described. Getting out is the same in reverse.

Driving, riding, sailing

They are one thing, and it is walking the room's map by a hull.

A vehicle has no exits of its own. It is standing in a room, and the ways out of a boat are the ways out of the water it is floating on — so the exits the player is offered while aboard are the room's, and taking one carries the vehicle with the player in it. There is one map, the rooms', whether the player is on their feet or afloat.

room drive "The Drive" {
  description: "Gravel."
  vehicle car "The Car" { description: "Cold vinyl."  aliases: "motor" }
  exit south -> garden                                             # feet
  exit north -> village if aboard(car) else "It is four miles."     # a hull
}

aboard(<vehicle>) is how a room says which of its ways out are for which. Without a condition an exit is for anybody, which means a car can be driven up a staircase if the staircase is an exit of the room the car is in — so the rooms where a vehicle lives are the rooms that want the condition, and they are exactly the rooms where the walking map and the driving map differ. playervehicle == "" is the other side of it, for a passage feet may take and a hull may not.

Nothing in the language distinguishes driving from riding from flying, because nothing needs to: they are the author's words for one act, and the words are the exit names.

Everything an exit can be, an exit a vehicle takes can be: a condition, a refusal in the game's own words, a target worked out while the game runs — exit north -> room("sea" + y + x) — or a block that runs instead of leading anywhere. open exit and close exit change the room's map, which is the map under the hull.

goto takes the vehicle with it. The player and the vehicle they are in are in the same room, always, and there are three ways that room changes — all of them keep it that way:

the player walks one of the room's exitsthe vehicle is driven, with them in it
the game says goto <room>the vehicle is carried, with them in it
the game says move <vehicle> to <room>the vehicle goes, and takes them if they are aboard

So goto village from a car puts the car in the village, and the player is still sitting in it. Nothing takes the player out of a vehicle by moving them: getting out is something the player or the game does — DISEMBARK, or disembark from a script — and it says so.

A question is the one place the two are briefly apart, and it is not a journey: the player asked from where they were, the boat stays floating where it is, and answering puts them back in the room it is standing in.

goto does not check moves if. That guard is on the passage the player asks for — walking a way out — and the game's own word is final, exactly as it is for move.

on arrive runs when the vehicle has got somewhere — driven, carried by a goto, or sent by a move. It is the one block a room has no equivalent for, and it runs after the vehicle's position changes, so roomof inside it answers where it has got to rather than where it came from. Driving then describes the vehicle and the room, as arriving anywhere does; a move says nothing of its own, so a move that should be announced is announced by the block:

on arrive { set fuel = fuel - 1
            if fuel == 0 { print "The engine dies as you arrive, and does not restart." } }

Three arrivals, and three placings that are not. Being driven, being carried by a goto and being sent by a move are passages, and each runs the block. The room that parks it when the world is built, spawn putting one on the board, and a restored save putting it back where it was left all set its position and run nothing: those are a vehicle being put somewhere. That is what makes move <vehicle> to room(<expr>) the statement to drive a computed map with.

A block that moves the vehicle again arrives again, which is a loop; it is stopped at sixteen with (runtime error: a vehicle arrived somewhere too many times; stopping.), the same cap a room's on enter has for the same reason.

The guards

A vehicle has four, and they are all the same shape as an item's takes if and a container's opens if: a condition read afresh at every attempt, and what the player is told when it refuses.

boards if <cond> else "..."may they get in?
moves if <cond> else "..."will it go?
reaches out if <cond> else "..."may they touch what is in the room around it? — see Seeing out of it, and reaching out of it
sealed if <cond>can they see out of it at all? — same section
bare if <cond>...and does it describe itself at all? — same section

The first three say locked, full, out of fuel, damaged, no driver and not yours — in the game's own words, with no state to keep in step:

boards if not car_locked else "The doors are locked, and the key is in the house."
moves if has(keys) else "You have no keys, and it is not that kind of car."
moves if npchere(bel) else "You can row. Not in this."
boards if crew < 3 else "There is no room in the boat."

A refusal costs a turn, the way a locked door does — all three of them. The else is required on all three too: a guard with nothing to say is a locked door with no sign on it.

sealed if and bare if are the odd two and take no else, because they refuse nothing: they are states the thing is in rather than answers to an attempt, which is the shape dark if has on a room.

There is no guard on getting out. A game that needs one puts the decision in on exit, which runs after the fact — which is how a ship drowns somebody who goes over the side at sea.

What is inside it

A vehicle holds things, the way a room does and a container does:

vehicle car "The Car" {
  description: "Cold vinyl."
  aliases: "motor"
  item starting_handle           # ...in it from the start
}

> enter car
INSIDE The Car
Cold vinyl.
In it: a starting handle.

OUTSIDE The Car
The Drive
...

What is in it is within reach of somebody sitting in it and out of reach of everybody else — the rule a shut container already follows. DROP inside puts the thing in the car, because that is where your hands are and a thing let go of in a boat does not land on the water. spawn x in <room> and move x to <room> name rooms, so a game that wants to load the boot moves the player into it or lets them do it. A dark room hides what is in the vehicle as it hides its own items: the contents of a car parked in an unlit shed are as unknowable as the shed's.

Seeing out of it, and reaching out of it

You can see out of a vehicle and you cannot reach out of it.

> enter car
> examine spanner
It's a spanner. Nothing special.
> take spanner
You would have to get out of The Car first.

The room is described, its items are listed and EXAMINE reaches them, because EXAMINE is looking and the whole of the second description is there to be looked at. TAKE, OPEN, CLOSE, READ, SEARCH, USE and PUT do not: sitting in a car is not a position from which to pick a tyre up off the gravel, and the refusal names the thing you would have to leave.

TAKE ALL aboard empties the vehicle, not the room — all is what you can reach.

TALK, ASK and ATTACK are not affected, in either direction: a voice and a pistol both carry, and a creature in the room can still reach you. A vehicle that made you untouchable in a fight would be a place to hide rather than a thing to ride.

So the wall is a wall in both directions for things, and transparent in both directions for sight and for anything that crosses a gap on its own.

reaches out if <cond> else "..." is the exception, for the cart you can lean out of. It is the only guard in the language that turns something on:

vehicle cart "The Cart" {
  description: "Planks."
  reaches out if side_down else "You cannot lean that far with the side up."
}

> take spanner
You cannot lean that far with the side up.
> lower the side
> take spanner
Taken: a spanner.

The else is required, as it is on boards if and moves if, and it replaces the engine's line — a vehicle that says why in its own words should not also be told you would have to get out.

sealed if <cond> is the other direction: a hull nobody can see out of.

vehicle sub "The Bathyscaphe" {
  description: "Rivets and a smell of oil."
  sealed if hatch_shut
}

> enter sub
INSIDE The Bathyscaphe
Rivets and a smell of oil.

Exits: north.

Sealed, there is no OUTSIDE label and no room at all: no description, no items, no people, no creatures. EXAMINE cannot see out, TALK and ASK cannot be heard through it, and nothing in the room can attack the player — which is the other half of their not being able to attack it, and the reason to write a submarine rather than a cart with the lid on.

It takes no else: it is a state the thing is in rather than a refusal, which is the shape dark if has for the same reason.

The ways out stay. A sealed hull still steers, and its Exits: line is still printed and still walked — the same rule darkness follows, and for the same reason: a way out is felt for rather than seen. A vehicle that should not be able to move says moves if false and says why.

sealed if and reaches out if on one vehicle are refused by the compiler: a hull nobody can see out of is one nobody can reach out of either.

bare if <cond> is the mirror of sealed if, and the other half of the pair. sealed if hides the room and keeps the vehicle; bare if hides the vehicle and keeps the room — for the cart with nothing to say about itself, whose rider wants to see the field they are crossing:

vehicle dray "The Dray" {
  description: "Straw, and a plank to sit on."
  bare if tarpaulin_off
}

> enter dray
OUTSIDE The Dray
The Lane
Hedges, and the smell of somebody's fire.
Exits: north, south.

The whole INSIDE block goes: the label, the description, In it: and With you:. Everything about the room stays, its own OUTSIDE label included — that label names the thing the player is sitting in, which has not changed, and without it two rooms in a row would read as two places.

It takes no else, for the reason sealed if does not, and it is read afresh every LOOK: a tarpaulin that comes off is a condition, not a declaration.

A vehicle with nothing to say needs no bare if. If it has no description: and nothing in it — no items, nobody riding, nothing hostile — the INSIDE block is skipped on its own, and so is the OUTSIDE label, so LOOK reads exactly as it does on foot. Mounting a horse declared

vehicle horse "the horse" { aliases: "horse" }

changes nothing about the lane you are standing in, which is the point: a heading over an empty paragraph is worse than no heading.

It is nothing to say, not no description. A mule with no description but a saddlebag in it still says In it: a saddlebag, because a thing LOOK would otherwise list must not be hidden — the same rule darkness follows from the other side. And this is why bare if still keeps the OUTSIDE label: there the author has hidden an inside that does have something in it, and the label is how the player is told they are still aboard.

Both at once is allowed and shows nothing but the ways out. A vehicle that is sealed and bare is a blacked-out van: there is nothing to say about the inside and nothing to be seen of the outside, so LOOK prints the Exits: line and stops. That is not refused, because a game may well want it — but it is worth knowing it is what you have asked for.

Who is riding in it

A hull holds people and creatures as well as things, and they are written the way a room writes them — a whole declaration, or a bare name that puts somebody from the cast in there:

room yard "The Yard" {
  description: "Gravel."
  vehicle van "The Van" {
    description: "Ribbed steel and a smell of diesel."
    aliases: "van"
    npc bel "Bel" { dialogue: "\"Keep driving.\"" }
    enemy rat "a rat" { hp: 6  attack: 1  defense: 0  aliases: "rat" }
  }
}

> board van
INSIDE The Van
Ribbed steel and a smell of diesel.
With you: Bel.
In here with you: a rat (6 HP).

They travel with it, which is the reason to write them there rather than in the room: a room keeps its own people and creatures when the hull drives off, and a hull takes hers with her.

The wall reads the same way from both sides. Whoever is riding in the hull is within reach of the player inside it and out of reach of everybody outside it; whoever is in the room is the other way round. So attack rat from the gravel is There's no "rat" here to fight, and talk to pat from the driving seat is There's no one called "pat" here — until the hull reaches out if, which opens both directions at once, because a window you can reach through is a window that can be reached through. A sealed hull reaches nobody and nobody reaches it.

Getting in is an arrival. A hostile creature in the hold strikes when the player climbs in with it, exactly as one in a room strikes when they walk into it, and its on turn ticks while they are aboard.

They can be put in one while the game runs, too. A hull is the second kind of place, so the three statements that put a person or a creature somewhere take one:

spawn bel in van            # ...as `spawn bel in hall` puts her in a room
move  dog to van
remove dog from van

vehicle names the hull the player is sitting in, the way room names the room they are standing in — and they are in both whenever they are aboard one, so a game needs both words:

spawn rat in vehicle        # ...in here, with them
move  bel to vehicle
remove rat from vehicle     # ...out of this one, and out of nothing else

in room could never mean it, and a game whose hulls are a series has no id to name. On their own feet it is a statement that cannot mean anything, and it says so rather than quietly doing nothing: (runtime error: "in vehicle" names the hull the player is sitting in, and they are on their own feet.) from room while aboard still means both places, because the player is in both.

A thing cannot: what is in a hull got there in somebody's hands, and move tyre to car says so. A series of hulls is not a place either — one of them is, and the game has to say which.

...and a hull can be asked what it is carrying, which is the same question a room answers:

npccount(van)      enemycount(van)      enemyname(van, 0)

itemcount, exitcount, regionof and the rest stay the room's: a hull has no ways out, its region is the region of the room it is standing in, and what is in its boot is reached by getting in.

And so are her rooms'. A hull with rooms of her own is still a thing standing in a room, so the rooms inside her are in whatever region she is in: a deck is in the harbour for as long as the ship is tied up in the harbour, and in no region once she has put to sea. playerregion and regionof(<room>) both answer that way, the region's on turn keeps running while the player is below decks, and the region's on enter and on exit fire when the hull crosses the line — not when the player steps aboard a ship that has not moved.

One person starts in one place, and a hull is a place: a room and a hull both naming them is refused, as two rooms naming them already were. A series of people or creatures belongs at top level either way and is placed with spawn.

Where a vehicle is

roomof(car)the room it is standing in, or "" for one that is nowhere
roomof(bel)"" while she is riding in one — a hull is not a room, and she is in no room
car is hereit is standing in the room the player is in
playerroomthe room — the same answer aboard or on foot
playervehiclethe vehicle the player is in, and "" when they are on their own feet
aboard(car)is the player in that one? playervehicle == "car", and the word a room's exit wants
playerregionthe region of the room, which follows a player who is riding because their room does
regionof(roomof(car))which part of the map it is parked in, whether or not the player is aboard
member_here(lifeboat)which member of a vehicle series is standing here, or -1

The room it is standing in says so, the way a room already says which items are lying in it and who is standing in it:

room drive "The Drive" {
  description: "Gravel, and a car nobody has moved in a fortnight."
  item tyre
  npc bel
  vehicle car "The Car" {            # ...and a car, standing in it
    description: "Cold vinyl."
    aliases: "motor"
  }
}

A vehicle has the two homes a creature has, and no third one. Either it is declared inside the room it stands in, as above, or it is declared at top level — where it stands nowhere at all until something puts it somewhere:

vehicle taxi "A Taxi" { description: "Diesel and pine air freshener." }

spawn taxi in room          # ...or in a named room
move  taxi to rank
remove taxi from room       # off the board: what is in it stays in it, and `spawn` puts it back

That is placement the way a creature's is, and a series has only the second of the two: it is declared at top level and spawned a member at a time, with spawn lifeboat[i] in <room>.

A vehicle never says where it is. It used to — at: drive, written on the vehicle — and there was a third form as well, the bare name in a room (room drive { vehicle car }) naming a vehicle declared elsewhere. Both are gone, and both for the reason item lamp and npc guard have always argued: one fact, one spelling. The bare name was the worse of the two, because a series fell through it — one line that read like the placement of one thing parked all three lifeboats, each answering to the same word, so the player could only ever reach the first.

vehicle car "The Car" { at: drive  description: "Cold vinyl." }
→ zaytunc: a vehicle does not say where it is; the room it is standing in does — declare it
  there, the way the room declares the creature standing in it. A vehicle declared at top
  level is nowhere until `spawn` or `move` puts it somewhere

A vehicle declared at top level is how a taxi waits to be called, and the three statements above are placement the way a person's is. They follow a person's rules: in inventory is refused, because there is no carrying a ship, and removing a vehicle the player is sitting in is refused too, where it stands, rather than tipping them out into nothing.

Several of a vehicle

A fleet is a series, exactly as eight rats are:

vehicle lifeboat[3] "Lifeboat " + (member + 1) {
  description: "Two feet of water in the bottom."
  aliases: "boat" + (member + 1)
}

room jetty "The Jetty" {
  description: "Planks."
  on enter { spawn lifeboat[0] in room }    # a member at a time, as a series always is
  exit sea -> open_water if playervehicle != ""
}

member is the number of the stamp, as it is in a creature series, and every site that names a member says which one: spawn lifeboat in room is the same error spawn mon in room is. A series is declared at top level and nowhere else — vehicle lifeboat[3] "..." { ... } inside a room is refused, because a room declares the one vehicle standing in it. Three boats that are all different are a group instead — group ferries { skiff, launch, steamer } — and size() and length() count either.

Several vehicles may stand in one room, and LOOK lists them all. If two of them answer to one word the player can only reach the first, which is the rule aliases already follow — so a fleet wants member in its aliases:, as above.

board lifeboat[i] puts the player aboard one of them, and it is how a captain shifts their flag: the game decides which hull they are standing on, rather than the player walking to it. A group of vehicles takes the subscript too (board ferries[1]), and so does the form that works the name out while the game runs.

A member that has never been placed is nowhere, and boarding one is refused with That is not here — the words anything not in the room is refused with. spawn it first.

Rooms of its own

A car is a thing you sit in. A ship is a place that moves, and a vehicle says which it is by whether it declares rooms:

room quay "The Quay" {
  description: "Stone, and a smell of fish."

  vehicle ship "The Perseverance" {
    description: "Tar and wet rope."
    aliases: "ship", "her"
    entry: deck                            # ...where boarding lands. The first, if unsaid

    room deck "The Deck" {
      description: "Planking, and the sea going past."
      controls: true                     # ...the quay's ways out are offered here too
      exit down -> cabin
      exit aft -> engine_room
    }
    room cabin "The Great Cabin" {
      description: "A stern window and a table bolted to the floor."
      item chart
      npc surgeon "The Surgeon" { dialogue: "\"Not now.\"" }
      exit up -> deck
    }
    room engine_room "The Engine Room" {
      description: "Heat, and a noise you feel in your teeth."
      dark if not lamp
      enemy rat "a rat" { hp: 6  attack: 1  defense: 0 }
      exit fwd -> deck
    }
  }

  exit north -> sea if aboard(ship) else "You would have to swim."
}

They are rooms. Exits, items, people, creatures, dark if, on enter, on exit, on turn, roomof, spawn, npccount — everything a room has and everything that reads one, because the compiler lifts them into the room table and only remembers which hull they belong to. Writing them inside the vehicle is how a vehicle is written inside its room and a person inside their vehicle: the nesting says who owns what, and the table says what it is.

And they belong to her. deck inside a ship is that ship's deck: two ships may each have one, and the id the room actually carries is ship.deck — which is also how anything outside her names it.

move crate to brig.deck            # ...which of the two decks, said plainly
goto ship.cabin
if playerroom == ship.deck { ... }

Inside the hull nothing is qualified: her rooms name each other by their short names, entry: names one of them, and an exit between two of her rooms is written as it would be anywhere. The qualified form is for everything else — and playerroom answers with it, because it is the room's name.

A series of hulls declares rooms like any other hull, and each of them gets a set of her own. vehicle boat[3] with a deck and a hold written in it is three boats with six rooms between them: the description is stamped out with the member's number in it, exactly as the hull's own is, and a crate stowed in one hold is in that hold and no other.

vehicle boat[3] "Boat " + (member + 1) {
  aliases: "boat"
  entry: deck
  room deck "Deck of boat " + (member + 1) {
    description: "Planking."
    controls: true
    sees out if true
    exit down -> hold
  }
  room hold "Hold of boat " + (member + 1) { description: "Casks."  exit up -> deck }
}

From outside, a room of one of them is named with the hull's number and then the room:

move crate to boat[0].hold         # ...the first boat's hold, and not the second's
goto boat[2].deck
if playerroom == boat[0].hold { ... }
if aboard(boat[1]) { ... }

The number is written out. boat[i].hold is refused: what is inside the brackets is worked out while the game runs, and a room is a name the compiler resolves before it — room(<expr>) is the form for a room named while the game is playing. Saying no number at all is refused too, because boat.hold is a room of a hull that is three hulls.

Where the player is, is still two facts. Aboard her, playerroom names the room they are standing in and playervehicle names the ship:

> board ship
The Deck
Planking, and the sea going past.
Exits: down, aft.
> down
The Great Cabin
A stern window and a table bolted to the floor.
You see here: a chart.

No INSIDE/OUTSIDE pair: they are not sitting in a thing, they are standing in her cabin. What she looks like from the quay is examine:.

Walking her, and sailing her

Aboard a hull with rooms the player has two maps within reach — her ladders and hatches, and the ways out of the room she is standing in, which are hers to be taken along. The rules below keep them apart: the first is walked, the second is sailed, and one room in her is where the two meet.

Two maps, and they meet in two places. Inside her, GO walks her own ways out — DOWN is the ladder, and nothing is sailed. A room with controls: true also offers the ways out of the room the hull is standing in, and walking one of those sails her — unless she has named her own words, for which see below; from anywhere else the game says which room does. No exit may cross the wall: a way out from her deck to the quay, or from the quay to her deck, is refused, and the two ways through are BOARD and DISEMBARK. She is left by the room she is boarded by.

Two maps, two lines, two verbs. At the wheel, LOOK names both — the room's ways out under Exits:, and where she may be taken on a line headed by the word that takes her:

Exits: down, aft.
Steer: north, east.

GO (and a bare direction) asks the room first and the hull second, which is what it always did. STEER <direction> is the hull, always — it never means the room and never falls back to it — and that is what makes a direction both maps claim sayable: a deck with its own east hatch keeps EAST for the hatch, and STEER EAST takes the ship east. Without the second verb that way out could not be taken from anywhere, by any spelling.

The hull may say it in her own words, the way she may already say getting off her in hers:

steer: "sail", "put about"       # ...and the line reads `Sail: north, east.`

Naming her words takes walking away from her — inside her. A hull with rooms that says steer: is one whose author has said how she is moved, so her map stops being offered to GO where her controls are: aboard her, a direction is a ladder of hers or it is nothing, and it answers the way any direction a room does not have answers.

> north
You can't go north from here.

Her line above says what does take her there. A hull that names no words is unchanged — GO from the room with her controls sails her, as it always has — and a vehicle with no rooms is unchanged whatever she names: she is a thing the player sits in, inside a room they are also in, and driving her is walking that room's map.

The line is headed by her first such word, capitalised, and by Steer: when she declares none — it is the command, which is the shape Exits: has. It appears only in a room that steers, is filtered exactly as Exits: is — a way out whose condition is false is not listed until it holds — and is gone entirely while moves if says she will not go, because listing a way for a hull that will not move is listing a shut door. Merging the two lines into one would make four ladders of them, and the first surprise would be a ship at sea.

Sailing moves the hull and nobody. The surgeon is in the cabin before and after; the player is on deck before and after; what changed is where the ship is. So no room's on enter fires and no region's blocks do — nobody walked anywhere — and the block for exactly this is the hull's own on arrive.

> north
[the ship stands out past the mole]
The Deck
Planking, and the sea going past.
Exits: down, aft.
Steer: south.

Seeing and reaching out of her rooms

A hull with no rooms carries sealed if and reaches out if herself, because there is one inside and one wall — see Seeing out of it, and reaching out of it. A hull with rooms has neither, so the same two facts are written on the rooms.

Seeing out of her, and reaching out of her, are facts about her rooms. The deck sees the harbour over the rail; the great cabin sees it through a window it cannot reach through; the hold sees nothing at all. No engine can know which, so each room says so itself:

flag warped = true
room quay "The Quay" {
  description: "Stone, and a smell of fish."

  vehicle ship "The Perseverance" {
    description: "Tar and wet rope."
    entry: deck

    room deck "The Deck" {
      description: "Planking."
      controls: true
      sees out if true                     # ...the quay is described under her name
      reaches out if warped                # ...and can be taken from, talked to, fought
        else "You cannot reach the quay from midstream."
    }
    room cabin "The Great Cabin" {
      description: "A stern window."
      sees out if true                     # sees, and does not reach: it is a window
    }
    room hold "The Hold" { description: "Casks." }
  }
}

sees out if <cond> is a state and takes no else, the shape dark if has. The other is a guard and must say what a refusal sounds like, the shape boards if has — and reaching implies seeing, so a room that reaches out needs only the one line.

> board ship
The Deck
Planking.

OUTSIDE The Perseverance
The Quay
Stone, and a smell of fish.
You see here: a crate.
Also here: The Docker.
Exits: down.

The room the hull is standing in is described under her name, after this room's own lines, with everything in it listed. Its ways out are not: the Exits: line is the deck's, because the quay's are the ship's to sail. Reach is mutual and reach is the whole of reach — a dog that can be kicked off the deck can bite the man on the deck, and a room that only sees gets the refusal that names the ship, or its own else. A room that says neither word is an interior room, and prints what it always printed.

Putting a cast or a cargo aboard

Moving things about inside her is the ordinary way. Her rooms are rooms, so spawn, move and remove against a room's id do what they do anywhere — move rat to cabin, move crate to cabin off the quay, move surgeon to quay without a gangway. No exit may cross the wall, and a statement may: the wall is about what the player may walk, and a statement is the author saying where things are.

What is refused is naming the hull where a room is wanted. move rat to ship, spawn bel in ship and remove rat from ship are refused where ship is a hull with rooms, and name one of her rooms instead: somebody in the ship is in no particular part of her, and a cast put there would be in a list no room describes and nothing can reach. For the same reason in vehicle, which means the hull the player is sitting in, is a runtime error while they are standing in one of hers — in room is the word for where they are.

in vehicle as a place keeps its meaning for the simple kind, and npccount(ship) is a question rather than a placement: it answers for either shape. For a hull with rooms it adds her rooms up, in the order she declares them — so npccount(ship) is everybody aboard her, npcname(ship, 0) is the first of them, and enemycount(ship) counts the living ones the same way. Asking a room of hers — npccount(ship.deck) — asks about that room alone, as it always did.

What a hull with rooms does not have

The guards about her outer wall are her rooms' now, or nobody's. sealed if is about the wall between a hull and the room it is standing in, and there is no such wall to be on one side of when the player is standing in her engine room: it is refused, and a room that cannot be seen out of is dark if, written on that room — or one that simply says nothing about seeing out. reaches out if written on the hull is refused too, and says which room to write it on.

A cast or a cargo of her own — npc, enemy or item written in the hull rather than in one of her rooms — is refused for the reason the statements above are: somebody in the ship is in no particular part of her.

A vehicle that declares no rooms is unchanged in every particular — the car, the horse, the INSIDE/OUTSIDE pair, GO driving, the cast, the guards, all exactly as above.

What a vehicle is not

People

An npc lives in a room and answers to talk. It has two parts, and the second is the one that does the work:

npc smith "Hedra the Smith" {
  dialogue: if gaveIron
    then "\"Shield holding up? Good.\""
    else "\"Bring me iron and I'll bank you a shield.\""
  on talk {
    if has(iron_ingot) and not gaveIron {
      print "She takes the ingot without a word and reaches for the tongs."
      set gaveIron = true
      remove iron_ingot from inventory
      spawn oaken_shield in inventory
    }
  }
}

A person's name is an expression too, worked out where it is shown, the same rule a room's title follows:

npc boss "the " + rank { dialogue: "Rank has its privileges." }
npc guard[3] "guard " + member { dialogue: "I am guard " + member }

Also here: guard 0, guard 1, the sergeant.
> promote
Also here: guard 0, guard 1, the captain.

The name the player can see is the name that reaches them: after the promotion talk to the captain works and talk to the sergeant does not. It was the one kind of title that had to be a literal, which npc guard[3] "guard " + member runs straight into — see A series of creatures.

examine: is what looking at them says. EXAMINE reaches a person the way it reaches an item, a creature and a vehicle, and this is what it answers with:

npc bel "Fisher Bel" {
  examine: "Oilskins, and hands you would not want closed on you."
  dialogue: "\"Mm.\""
}

> examine bel
Oilskins, and hands you would not want closed on you.

It is an expression like every other description, so it follows the state of the game, and a series stamps member into it. A person with no examine: answers with their name — It's Fisher Bel. — which is the same thin answer a creature with no examine: gives.

dialogue: is an expression, so what somebody says can depend on the state of the game — that is how an NPC notices you are carrying what they asked for. on talk runs afterwards, and is where the exchange happens: taking the thing, handing over the reward, setting the flag that opens the road. Talking again re-runs both, so guard one-shot exchanges behind a flag, as above.

Players reach an NPC by its id or its full display name — talk smith and talk hedra the smith both work. aliases: adds any other names that should find them, which matters as soon as a display name is longer or stranger than a player wants to type:

npc luthien "Lúthien" {
  aliases: "luthien", "tinuviel"
  dialogue: "\"Tinúviel,\" she says. \"Keep it.\""
}

Now talk luthien and talk tinuviel both reach her, and so does ask tinuviel about huan. Matching is case-insensitive. NPC ids are game-wide, because move and npcin can name one from anywhere.

Topics

talk is the opening line. ask <someone> about <something> is the conversation:

npc guard "the guard" {
  dialogue: "\"State your business,\" says the guard."
  topic "name", "odill" { print "\"Odill. Third son of a fourth son.\"" }
  topic "yard", "castle" { print "\"Nothing in the yard but weather and me.\"" }
  otherwise { print "The guard looks at you for a while and says nothing at all." }
}

> ask guard about the yard
"Nothing in the yard but weather and me."

A topic lists one or more keywords and a body of ordinary statements, so a topic can set flags, hand over items or open a route — a conversation is a way of changing the world, not just of printing at it.

Leading articles are ignored on both sides, as they are for every other name in the language: about the yard finds "yard", and about bridge finds "the bridge". A keyword may carry its own article or not, and the player may type one or not, and all four combinations meet.

otherwise catches anything nobody has an answer for. Without one, asking about an unknown subject prints "<name> has nothing to say about that."

Because topic bodies are statements, branching dialogue is just flags: gate a topic behind if, and have one topic unlock another by setting something the next one tests.

Telling the player what is worth asking

A player cannot guess a subject they have never heard of, and until they do, the best writing in a game sits behind a word nobody types. suggest opts a topic into the TOPICS listing:

npc guard "the guard" {
  dialogue: "\"State your business,\" says the guard."
  topic "yard", "castle" suggest { print "\"Nothing in the yard but weather and me.\"" }
  topic "captain" suggest when met_captain { print "\"He is not a bad sort.\"" }
  topic "odill" { print "\"How do you know my name?\"" }
  otherwise { print "The guard looks at you for a while and says nothing at all." }
}

> topics guard
the guard will talk about: yard.

Three rules, and each is there for a reason:

TOPICS on its own covers everybody in the room. It costs no turn, and it only appears in HELP for games that suggest something.

Near misses on a topic

Asking about a subject that does not exist and asking about a real one by the wrong name used to read identically. They no longer do: if what the player typed is a plural of a keyword, or a prefix of one from four letters up, the game says so without saying which.

> ask guard about yards
The guard looks at you for a while and says nothing at all.
(You have the right idea. Try another word for it.)

The rule is deliberately narrow: it catches the misses of grammar and nothing else. It cannot cover a synonym you did not think of, and the fix for those is to think of them — keywords are cheap, and topic "yard", "yards", "courtyard", "court" costs nothing and saves a player from a wall.

Declaring people once, and placing them

A person can be written inside the room they start in, which declares and places them at the same time:

room yard "The Yard" {
  npc guard "the guard" { dialogue: "\"State your business.\"" }
}

Or declared at top level, like an enemy in a bestiary, and placed by whichever room they start in — npc guard, exactly the shape item lantern has:

npc guard "the guard" {
  aliases: "guard"
  dialogue: "\"State your business,\" says the guard."
  otherwise { print "The guard says nothing." }
}

room yard "The Yard" {
  description: "Barrels and a weighbridge."
  npc guard
}

The two forms produce the same person. Top level is worth it when somebody appears in the middle of a story rather than at the start of it — declare them there, place them nowhere, and move them in when their moment comes. Before this existed the only way to do that was to park them in a room with no way into it.

A person may be placed by at most one room, and their id has to be unique across the whole game, both of which the compiler checks. Somebody declared at top level and never placed simply is not anywhere until a move or a spawn puts them somewhere.

Bringing people in, and taking them out: spawn and remove

spawn <npc> in room      # they are now in the room the player is standing in
remove <npc> from room   # they are now nowhere

remove takes a person out of wherever they are, not merely out of the room the player happens to be standing in — which is the one place its meaning differs from an item's. Being somewhere is the whole of a person's placement, so "not in this room" and "not in the story" are the same statement, and the usual case is removing somebody the player cannot see:

on turn {
  if betrayed and not gorlim_gone {
    set gorlim_gone = true
    remove gorlim from room      # wherever he was, he is not there now
  }
}

Neither form takes in inventory. There is no sense in which you carry somebody, and the compiler says so rather than letting it mean nothing.

A person who has been removed still exists — spawn puts them back, and their state is unchanged — so this is "off stage", not "deleted". If you want somebody gone for good, remove them and never spawn them again.

People who move

Where somebody is standing is state, not a fixed property of their declaration. move <npc> to <room> sends them somewhere, and two expressions ask where they are:

npchere(npcId)is this person in the player's room?
npcin(npcId, roomId)is this person in that room?

Put move in on turn and they walk a route whether or not the player is watching:

on turn {
  set beat = beat + 1
  if beat >= 3 {
    set beat = 0
    if npcin(guard, gate) {
      move guard to yard
      if npchere(guard) { print "The guard tramps in from the gate." }
      else { print "Boots recede towards the yard." }
    }
  }
}

An NPC still starts in the room it is declared in. move is the only thing that changes that — and since talk and ask only reach people in the player's room, someone can walk out in the middle of a conversation.

Enemies

An enemy is the other kind of inhabitant a room can have. Where a person is someone to talk to, an enemy is something with hit points that can be fought — and like a person, where it stands is state rather than a fixed property of its declaration.

room bridge "Rope Bridge" {
  description: if alive(goblin) then "A goblin blocks the way." else "The bridge is clear."
  exit north -> shrine if not alive(goblin) else "The goblin won't let you pass."
  enemy goblin "Goblin" {
    hp: 12
    attack: 3
    defense: 0
    aliases: "monster"
    examine: "A wiry goblin gripping a rusty dagger."
    hostile: true                                      # attacks on entry; an expression, see below
    ambush: "The goblin springs from behind a post!"    # its line when it does
    retaliates: true                                   # does it hit back? (the default; see below)
    on defeat { print "It dissolves into shadow." spawn goblin_ear in room }
    on attack { print "The goblin bares its teeth." }   # runs each round before damage
    on turn   { print "It sniffs the air." }             # every turn it is alive and here
  }
}

alive(enemyId) asks whether it is still standing, and ehp(enemyId) gives its current hit points. Both work from anywhere, so a description, an exit condition or an on turn block can all depend on whether something is dead yet.

The ceiling those hit points are held to

hp: is worked out when the creature is spawned and becomes its ceiling, the way maxhp is the player's — so set ehp(x) = 300 on something spawned at 50 leaves it at 50. maxehp(x) reads that ceiling and set maxehp(x) = n moves it:

set maxehp(hulk) = 300        # ...and now it can be healed that far
set ehp(hulk) = 300

Raising it leaves the creature where it is; lowering it brings the creature down with it, which is the rule maxhp and every stat ceiling already follow. A ceiling of nought or less is held at one: a creature with no room for hit points is not a creature.

This is what a series re-used for something else needs. Eight slots standing for whatever is on the board this week are eight creatures whose size was decided the first time each was spawned, and without a way to move the ceiling the ninth thing to use slot 3 is the size of the first. The other answer is to remove the member and spawn it again, which gives a fresh instance and a fresh ceiling; maxehp is the one that does not throw away the wounds.

x.maxhp is the same number under the creature's own-number spelling — see A thing's own numbers. Two spellings, one rule.

Hostile enemies

By default an enemy waits to be attacked. hostile: true makes it strike the moment the player walks into the room, and again on every re-entry while it's alive: the room is described first, then the enemy gets one round — its on attack block, then its damage. ambush: replaces the default "The Goblin attacks you!" line with an expression of your own. An ambush can kill, so it runs on death like any other damage.

It is an expression, not a fixed property, and it is read afresh on every arrival, so a creature can be peaceable until the story says otherwise:

enemy guardian "the guardian" {
  hp: 40  attack: 5  defense: 0
  hostile: angered
  ambush: "The guardian moves."
}

Walk in before the idol is taken and nothing happens; take it and walk in again and the guardian is waiting. Written as a literal that had to be true from the first line of the game, "asleep until you touch something" had to be built out of provoke and a flag in the room's on turn.

ambush: is the line, not the switch. hostile: and retaliates: beside it are booleans, so ambush: true is an easy thing to write and means nothing anybody wants — the creature announces itself with the word true. The compiler warns about it; see Warnings.

In a series, hostile: angry[member] gives each member its own switch, so one can wake while the rest sleep — see A series of creatures.

Creatures are asked one at a time and in the order the room lists them, and one that is not hostile is passed over rather than ending the round — so a peaceable thing standing in front of an angry one does not shield you from it.

Hitting back: retaliates:

enemy dummy "a straw dummy" { hp: 30  attack: 4  defense: 0  retaliates: false }

Left unsaid it is yes, which is what every creature has always done: you hit it, and if it survives it hits you. Said no, you get something that can be hurt for nothing — a training dummy, a chained thing, a creature the story wants harmless until it is not.

> attack dummy
You hit a straw dummy for 3 damage.

It is an expression, not a fixed property, so it can change while the game runs:

enemy sleeper "a sleeper" { hp: 30  attack: 4  defense: 0  retaliates: not asleep }

That is the free first blow: strike it while it sleeps and nothing comes back, wake it and the fight is ordinary.

> attack sleeper                    (asleep)
You hit a sleeper for 3 damage.

> attack sleeper                    (awake)
You hit a sleeper for 3 damage.
A sleeper hits you for 4 damage. (36/40 HP)

It only governs the blow back. A creature that never retaliates can still be made to strike with provoke, and hostile: still makes it attack on entry — see below for both. The three are separate switches: hostile: is does it open the fight, retaliates: is does it answer one, and provoke is make it strike now.

Who is striking: attacker

An on attack block runs before the blow is worked out, which is what makes it useful: what it changes is what lands. A weapon's damage: is the weapon's own — see A thing's own numbers — so a gun that has run out says so by setting its own damage to nothing:

flag rounds = 6
item pistol "a service pistol" { weapon: true  damage: 14 }

enemy patient "a patient" {
  hp: 22  attack: 9  defense: 1
  on attack {
    if attacker == "player" and equipped(pistol) {
      if rounds > 0 { set rounds = rounds - 1  set pistol.damage = 14 }
      else { print "The pistol clicks. Empty."  set pistol.damage = 0 }
    }
  }
}

The round is counted where it is spent, and both ways round are written every round, so reloading needs no second place to remember the number. An empty gun adds nothing to the arm holding it — the swing is the player's own attack: and no more, and is answered like any other swing. A gun that should not be swingable at all when empty is an ordinary if in the verb that swings it.

refuse calls the blow off. Written inside on attack, it means this blow does not land: nothing is hit, nothing is announced, and the creature does not strike back at a blow that never happened. It is how a game prints a message instead of striking:

on attack {
  if attacker == "player" and sparing { print "You cannot bring yourself to."  refuse }
  if attacker != "player" and asleep  { refuse }          # it does not wake
  if chance(20) { print "It glances off the plate."  refuse }
}

It is a sentence about a blow, so it can only be written where there is one: refuse outside an on attack block is a compile error rather than a line that does nothing.

finish in the hook calls the blow off too. A hook that ends the story — by finish, or by hurting the player to nothing and reaching on death — stops the swing it was called from as well as the rest of the block, so nothing is announced and nothing is hit under the ending. That is worth knowing when the hook does its own arithmetic: the statements after the finish do not run, so a refuse written at the bottom of the block will not be reached, and does not need to be.

attacker says who is striking: "player" when the player swings, and the creature's id when it strikes back. on attack fires for both directions — it is a round of combat involving that creature, not the player's swing alone — so a hook that means only one of them has to say which:

on attack { if attacker == "player" { ... } }

Without that guard, being hit spends the player's ammunition too, and empties the magazine twice as fast. attacker is read-only.

A creature's hit points are written the same way, with set ehp(<creature>) = <expr>:

set ehp(wolf) = ehp(wolf) - 6         # something hurt it that was not you
set ehp(wolf) = 0                     # and something finished it
set ehp(mon[2]) = 40                  # one member of a series

The rules are the player's rules, so the two do not have to be learned separately: the value is clamped to that creature's own starting hit points, a negative write is nothing rather than a negative number, and crossing to nothing runs the creature's on defeat, exactly as the player reaching nothing runs on death. It runs once for one death — writing 0 to a creature that is already beaten does nothing — and a creature written back above zero is alive again and can be defeated again.

No combat text is printed. You have defeated the wolf! belongs to the blow that earned it; a game writing hit points deliberately says what happened in its own words.

This is what makes a weapon that consumes something, a trap, a poison, a falling rock or a second creature's attack expressible: before it, the only thing in the language that could reduce a creature's health was the player swinging at it.

Making something attack: provoke

hostile: covers one case — it strikes when you walk in, and only then. For everything else there is provoke, which makes a creature strike now:

item idol "a jade idol" {
  on take {
    print "The stone grinds behind you."
    provoke guardian
  }
}

It goes through exactly the same round an ambush does: the creature's own ambush: line (or the default one), then its on attack block, then its damage, and on death if that kills you. One implementation, so the two cannot say different things.

The name is provoke rather than attack because the direction matters: attack guardian is what the player types, and this is the opposite.

It names a creature the way every other statement does, so one member of a series, or a name built while the game runs, is provoked as readily as a single creature:

provoke guardian
provoke mon[2]
provoke mon[i]
provoke enemy("mon" + i)

A series is an array and the subscript says which of them — the members are separate creatures with separate hit points, so provoking one leaves the rest alone. A bare provoke mon is refused for the same reason spawn mon in room is: it hides which creature the line is about. A subscript that lands outside the series provokes nothing, which is the answer every other out-of-range subscript gives — see A series of creatures.

A creature can only be provoked so many times over. A provoke inside an on attack block runs that block again, so a creature that provokes itself — or two that provoke each other — used to run until the stack did. Sixteen deep it stops and says (runtime error: a creature was provoked too many times over; stopping.), the same answer a proc that calls itself forever gets.

The same bound is on arriving somewhere: a room whose on enter sends the player back to it, or two rooms that send each other, stop after sixteen with (runtime error: a room sent you somewhere too many times; stopping.). A chain of trapdoors is fine and always was — the bound is on the cycle. And an on death block that takes the last hit points again does not die twice: you die once, however many times the block asks.

It has to be alive; it does not have to be in the room. A dead thing cannot hit anyone, so provoking one does nothing. But a statement you wrote should do what it says, so a creature next door still strikes rather than the line silently doing nothing — bring it in first with move guardian to shrine if you want it present.

Attacking every turn from now on is this and a room's on turn, which is why there is no second kind of hostility to learn:

flag angered = false

room shrine "Shrine" {
  description: "A stone shrine."
  on turn { if angered { provoke guardian } }
}

Since ambush: is an expression, it can tell the first blow from the rest:

ambush: if angered then "The guardian swings again." else "The guardian's eyes open."

Declaring enemies once, and placing them

An enemy declared inside a room starts in that room. Declared at top level it is a bestiary entry: a template that exists but is nowhere until something places it — the same shape item lantern has, and the same shape a top-level npc has.

enemy prowler "a prowler" { hp: 12 attack: 3 defense: 1 }

Bringing enemies in, and taking them out: spawn and remove

spawn prowler in room          # places it here, freshly rolled, at full health
remove prowler from room       # takes it away again
remove prowler from crypt      # ...or out of a room you are not standing in

spawn rolls a fresh creature: its stats are evaluated at that moment and it arrives at full health. That is the right thing for a wandering monster and the wrong thing for one the player has already fought — see below.

An enemy can only be spawned in room, never into the inventory, and the compiler says so rather than letting it mean nothing.

remove takes a named room as well as room. from room is the room the player is standing in; from crypt is the crypt, wherever the player happens to be. That matters more than it looks: a game that clears last round's creatures out of an arena it is not standing in — a between-rounds reset, a new wave, a fresh sortie — could only ever say from room, which meant here, so it removed nothing and the old creatures were still waiting when the player came back. It compiled, it ran, and it was quietly wrong.

A creature is in one place or nowhere, so naming a room it is not in does nothing, the way removing an item from a room it is not in does nothing.

For a person, from room still means the whole story. Being somewhere is all of an NPC's placement, so remove porter from room takes them out of every room there is — which is what it has always done and what games in the wild expect. Name a room and you get that room only:

remove porter from room        # out of the story
remove porter from kitchen     # out of the kitchen, and still wherever else they are

Enemies that move

move <enemy> to <room> relocates the live creature, and that is the difference from spawn and remove, which make and unmake. A moved enemy keeps the hit points it has taken, so something you wounded and that follows you arrives still wounded — where spawn would give you a fresh one at full health, the same name wearing a different creature.

verb chair {
  if alive(creature) {
    print "It comes through with you."
    move creature to nineteen_ninety_eight
  }
  goto nineteen_ninety_eight
}

> attack creature
> sit in the chair
It comes through with you.
1998
Enemies here: the loose creature (24 HP).

move names a live inhabitant — a person or a creature — so an item is a compile error rather than a silent no-op. Use spawn and remove for those.

Stats that are not fixed

hp:, attack: and defense: take expressions, and so does the name, which is what makes a procedurally generated bestiary possible:

enemy prowler monsterName {
  hp: 5 + depth * 4 + random(0, 4)
  attack: 2 + depth
  defense: depth / 3
}

These are evaluated once, at the moment the enemy is spawned, and the results are kept on that instance. A monster you leave wounded is still itself when you come back, even if the flags that produced it have since been rolled for something else.

A series of creatures

Slots hold values — numbers, strings and booleans. What they cannot hold is a declaration: rooms, items, people and creatures are named rather than computed, and spawn takes an identifier, not an expression. So where you want eight of something, the eight are declared as a series and the subscript goes on the name:

enemy mon[8] monsterName { hp: monsterHp attack: monsterAtk defense: monsterDef }

Every site that names a member says which one. spawn mon in room and alive(mon) are errors: an author who wrote mon[8] has said there are eight of them, and a form that quietly picks one hides which creature a line is about. A room that holds one already keeps a flag for it, so the number is there to use:

room c3 c_name[3] {
  description when alive(mon[3]): "Something in here has already noticed you."
  on enter { if not stocked[3] { set stocked[3] = true  spawn mon[3] in room } }
}

spawn mon[3] beside stocked[3] says chamber three holds monster three, on the page, with nothing to look up.

member is which one this is. A series is one description stamped out eight times, and member is the number of the stamp — 0 to 7 — readable in any field of the declaration. Without it every stamp is identical, which is what made a series feel like eight of exactly the same thing:

enemy mon[3] "mon number " + member {
  hp: 4 + member * 3
  attack: 1  defense: 0
  hostile: angry[member]
  ambush: "Number " + member + " lunges."
  on turn { print "mon " + member + " twitches" }
}

Enemies here: mon number 0 (4 HP), mon number 1 (7 HP), mon number 2 (10 HP).

hostile: angry[member] is the useful shape: one flag per member, so rousing one leaves the rest asleep. The same trick gives each a different name, different hit points, or a different line — one declaration, and the members are no longer interchangeable.

It is substituted for its number when the series is stamped out, so it costs nothing at run time and nothing in the file: member is not a value the game carries, it is a number written into each copy while the game is compiled. Written where there is no series it is a compile error, rather than the nothing it would otherwise mean.

member_here(mon) asks the series a question rather than standing in for a member. It gives the subscript of the member alive in this room, or -1 when there is none — useful when a creature has moved and the room no longer knows which of them is in it:

print "It has " + ehp(mon[member_here(mon)]) + " left."

mon[-1] reaches nothing, the way any out-of-range subscript does, so an empty room answers 0 rather than needing a guard. It takes the bare name for the same reason size(mon) does: both are about the series itself, and answer with a number.

That is eight creatures from one description, and these take a subscript on them:

spawn mon[i] in room
remove mon[i] from room
move mon[i] to cellar
provoke mon[i]
alive(mon[i])
ehp(mon[i])
noun_is_enemy(mon[i]) / other_is_enemy(mon[i])
set mon[i].hp = n / set ehp(mon[i]) = n

alive(mon[3]) still answers from anywhere, which is what it is for: a game asking whether the thing guarding a room four rooms away is dead does not care where the player is standing.

provoke mon without one is refused, for the reason every other bare name is: it hides which creature the line is about. hostile: angry[member] is the other way to make one strike — a flag per member, so rousing one leaves the rest asleep — and provoke mon[i] is the way to do it from a script; see Making something attack: provoke.

A series is a mould: one description, stamped out eight times. It suits creatures exactly, because eight rats really are one description and each still needs its own hit points and its own place — which nothing else in the language can express.

Items are not declared this way. item potion[8] is refused, and the compiler names the two things to do instead. Eight interchangeable things are one item with a count:, which stacks and splits and can sit in two places at once. Eight different things are eight declarations with a group over them, which keeps each one's own title and aliases. A group's members may be containers; a mould's could not be, because contains: names particular items and all eight members would claim the same ones.

Rooms are not declared this way either; no form that names a room takes a subscript yet.

People can be a series too, and that is what makes a party writable:

flag joined[3]
npc member[3] "a companion" { dialogue: "With you." }

proc recruit(i) { set joined[i] = true  spawn member[i] in room }
proc follow(i)  { if joined[i] { move member[i] to road } }

Seven things take a subscript on a person: spawn, remove, move, npchere, npcin, noun_is_npc and other_is_npc — recruiting, following, and asking who is here.

Most people in a game should not be a series. A series is for members that are interchangeable, and a character's whole point is that they differ — where Beren and Lúthien numbers its people (luthien, luthien2, luthien3, luthien4) those are one character with different things to say in different acts, 36, 21, 27 and 82 lines of it, and one shared description is the last thing they want. A party of recruits is the case this serves.

A series goes at top level and is placed with spawn, for people as for creatures — declaring one inside a room would mean all eight standing in it, and the compiler says so: "mon" is a series, so it belongs at top level and is placed with spawn.

Subscripts start at 0, a constant one outside the series is refused when the game is compiled, and a computed one outside it does nothing — spawn mon[9] on a series of four places nobody, and alive(mon[9]) is false. Those are the rules a flag's slots follow.

Items

An item is a thing in the world: something to pick up, open, read, search or use. Like a person or a creature, it is declared once and placed wherever it starts:

room cell "A Cell" {
  description: "Four walls of damp brick, and a loose stone in the floor."
  item loose_stone            # places an already-declared item here
}

item loose_stone "loose stone" {
  aliases: "stone", "floor"
  fixed: true                 # scenery: it can be examined and used, but not carried
  examine: "One brick sits proud of the rest."
  on use { print "You lever the stone up." }
}

An item declared at top level and never placed by a room is nowhere until a spawn puts it somewhere — the same rule people and creatures follow. Listing it in more than one room is not an error: it means several of it, one per placement — see Several of a thing. A container is the exception, since a second one would have to hold the same things the first does.

What an item can do is declared by giving it the block for that thing, rather than by setting a property that has to be kept in step with it. on use is what makes an item usable, on search what makes it searchable, and a text: is what makes it readable. Openness is the one exception, because it is a state rather than a thing that happens: four different declarations each make an item openable — see Openable things. That is also how HELP knows which commands to offer — see Help.

Each of those blocks has a section of its own: On take, On drop and On use below, then on open and on close under Openable things, on read under Readable things, on search under Searching, on equip and on unequip under On equip and on unequip, and an item's on turn — which ticks while the player can reach the thing — under Time.

fixed: true       # cannot be picked up; still examinable, usable, readable
spawn itemId in room / spawn itemId in inventory
remove itemId from room / remove itemId from inventory
remove all itemId from ...        # all of that one thing
remove all from room / remove all from inventory   # every item there, whatever it is

Unlike a person or a creature, an item can be spawned straight into the player's hands — which is the one place its rules differ from theirs.

spawn copies; move relocates. spawn brings a thing into being where it is pointed and leaves whatever was already elsewhere alone — that is what stocks a shelf or drops loot. move takes the one that already exists somewhere and puts it here, which is the sentence to reach for when there is one lamp and it should now be in the vault:

spawn lamp in vault      # a lamp in the vault; any other lamp stays where it is
move lamp to vault       # THE lamp, out of wherever it was, now in the vault
move lamp to inventory   # ...into the player's hands
move lamp to room        # ...into the room the player is standing in

move is the same statement that relocates a person or a creature, and takes the same three places for a thing. People and creatures go in a room and nowhere else, so move guard to inventory is a compile error rather than somebody in the player's pocket.

A stack is moved whole. A thing can be in several places at once when it declares a count: or is written into several rooms, and move is a sentence about the thing rather than about one of the places it is lying in: three coins here and three in the vault, moved to the pack, arrive as six.

remove all <itemId> takes all of that one thing, where remove takes one of it:

remove stone from room           # one of the six
remove all stone from room       # all six, and nothing else in the room

remove all from <place> empties it, whatever is in there:

verb wake { remove all from inventory  print "You wake with nothing but the clothes on you." }

remove all from inventory     # the pack
remove all from room          # the floor here
remove all from cell_block    # ...or a room the player is nowhere near

It reaches the counts as well as the listing, so a stack goes to nothing rather than leaving a number behind for a thing that is not there, and anything that was equipped stops being equipped because it is no longer carried. Emptying a place that is already empty does nothing.

The two are told apart by whether a name follows all. That is also what leaves an item called all mostly usable: it can be spawned and named like anything else, and only remove all from ... is closed to it, because there is nothing between all and from to read as a name.

room and inventory are not the only places. Either statement takes a room outright, the way move always has, so a thing can be put somewhere the player is not:

spawn coin in vault              # that room, wherever the player happens to be
spawn coin in room("cell" + n)   # a room worked out while the game runs
remove coin from vault

room still means the room the player is standing in and inventory still means their pack; those two words win, and a game that has a room actually called room or inventory reaches it as room("room"). People and creatures can be spawned into a named room too — but removing one is not something a room can scope: the runtime takes them out of every room, because for them "not in this room" and "not in the story" are the same statement. remove <npc> from vault is therefore a compile error telling you to write remove <npc> from room.

Spawning something that can be several adds one more of it. spawn arrow in inventory twice leaves two arrows, not one arrow listed twice, and matches what placing it twice in a room means — see Several of a thing. For an item that is only ever one of itself, spawning it where it already is does nothing.

On take

item journal "a water-stained journal" {
  aliases: "journal"
  on take { print "The pages stick together as you lift it." }
}

It runs after the item is in the pack, and under the line the player already saw: Taken: a water-stained journal. prints first, then the block. has(journal) is true inside it. The block is where a game reacts to a taking, not where it decides whether to allow one — deciding is takes if, below.

On a fixed: item it answers instead. Scenery cannot be picked up, so a fixed item with no on take says You can't take that., and one with a block says whatever the block says while nothing moves — has(x) is still false inside. That is how a bolted-down thing explains itself in its own words:

item slab "a marble slab" {
  fixed: true
  aliases: "slab"
  on take { print "It weighs what a slab of marble weighs." }
}

A stack is taken whole, so TAKE ARROWS on twelve of them runs the block once, with count(arrows) already at twelve — see Several of a thing. Lifting something out of a container runs it too, and that is take arrows from quiver.

Only the player's own TAKE runs it. spawn <item> in inventory and move <item> to inventory fill the pack without it, because those are the game's own doing and it already knows what it meant by them. A game that wants both writes both.

On drop

item censer "a swinging censer" {
  aliases: "censer"
  on drop { print "The coals scatter across the flags and go out." }
}

It runs after the item has left the pack — inside the block has(censer) is false and here(censer) is true, the mirror of on take. It prints under the Dropped: line, and like on take it answers only to the player: remove <item> from inventory does not run it. Whether the drop is allowed at all is drops if — see below.

A fixed: item never runs one. It cannot be carried, so it can never be dropped, and the block would be a line that never executes. on take is the half of the pair scenery can answer.

Whether it may be taken, dropped, worn

An on take block runs after the taking. That is the right shape for reacting to one and the wrong shape for deciding it, so the deciding is written as a guard, in the same words a lock already used:

item cloak "a velvet cloak" {
  drops if playerroom == "cloakroom"
        else "This isn't the best place to leave a smart cloak lying around."
  on drop { set bar_lit = true }     # only runs on a drop that was allowed
}

There are four, one per action, and every one reads like opens if:

takes if <cond> else "<message>"whether TAKE may pick this up
drops if <cond> else "<message>"whether DROP may put it down
equips if <cond> else "<message>"whether EQUIP may put it on
unequips if <cond> else "<message>"whether UNEQUIP may take it off

The guard is asked first, and nothing happens if it says no. Nothing moves, no Taken: or Dropped: line is printed, and the matching block does not run — which is the point: a handler no longer has to begin by asking whether it should be running at all.

The condition is re-checked every time, exactly as a lock's is, so a cloak that may not be dropped in the bar may be dropped in the cloakroom a moment later.

The else is an expression, read afresh when the player is told it, so the reason may change with the situation — or be left out, in which case the engine says the plain thing: You can't take that., You can't drop that., You can't equip that., You can't unequip that.

A full slot asks two guards. EQUIP with something already in the slot takes that thing off first, so the equip needs the newcomer's equips if and the displaced item's unequips if. A blade that will not be let go of stops the sword going on — which is the only reading of the two that is not a contradiction.

Guards answer the player, not the game. spawn, move, remove and the equip/unequip statements are the game's own doing and are not asked, exactly as the handlers are not run for them. A game that wants both writes both.

What a guard is not. It does not hide the item, and it does not stop anything else reaching it: take is refused, examine is not. And takes if on a fixed: item is a compile error, because scenery can never be taken and the guard could never be asked.

On use

item lever "a rusted lever" {
  fixed: true
  aliases: "lever"
  on use { print "The lever gives, and somewhere below, water starts to move." }
}

Declaring the block is what makes the item usable. There is no usable: property to keep in step with it, and an item without one answers USE with Nothing happens.

The player does not have to be carrying it. USE LEVER works on a thing lying in the room, which is what makes scenery usable at all — a lever bolted to a wall is the ordinary case, not the exception. Where it matters that the thing is in hand, the block is the place to say so:

on use {
  if not has(rope) { print "It is over there, not in your hands." }
  else { print "You pay out the rope into the dark." }
}

An item that runs out is still used through this block. uses: counts the block's runs down and empty: is what the player gets once there are none left, in place of it — see A thing that runs out.

Openable things

Four things make an item openable, and any one of them is enough: open:, a lock written with opens if, an on open block, or an on close block. An openable item tracks its own open/closed state, which any expression can test with isopen(itemId), making it a natural gate for exits and descriptions:

room cell "Cell" {
  description when isopen(door): "The cell door hangs open."
  description: "A bare cell. The door is shut."
  exit north -> corridor if isopen(door) else "The door is shut."
  item door
}

item door "iron door" {
  fixed: true
  on open { print "The hinges shriek." }
}

One block is enough: the door above answers to close too, just without a line of its own. Anything that says none of the four is not openable, and open/close on it say so.

A door that opens and shuts and says nothing needs no block at all. open: false is a whole declaration — the field says which state it starts in, and having a state at all is what makes it a thing that opens:

item hatch "a floor hatch" { fixed: true  open: false }   # opens, shuts, narrates nothing
item cell_door "the cell door" { fixed: true  open: true }  # ...standing open at the start

A lock is enough on its own too, and this is the one worth knowing about, because it used to be a trap. opens if on an item with no block once compiled to a thing that could not be opened — so the lock guarded nothing, the contents were listed on look and could be taken, and neither the compiler nor the player said a word. The same lock either shut the chest completely or did nothing at all, depending on whether an unrelated on open happened to be written beside it:

item chest "an iron chest" {
  fixed: true
  contains: brass_key
  opens if has(crowbar) else "The lid will not shift."
}

That chest is shut, and the key inside it is out of reach until the crowbar is found — with no block written anywhere.

Containers

An item can hold other items. Declaring contains: is what makes something a container, and there is no separate flag to keep in sync. Whether that container is one you have to open is a separate question, answered by the four things above: a container that says none of them is always transparent, and one that says any of them starts shut.

item chest "iron chest" {
  fixed: true
  contains: brass_key, folded_note
  opens if has(crowbar) else "The lid will not shift. You would need something to lever it."
  on open { print "The seam gives with a bang." }
}

item basket "wicker basket" {
  contains:                       # a container that simply starts empty
}

opens if <cond> else "<message>" is a lock. The condition is re-checked on every attempt, so a chest can start shut and become openable the moment the player finds the key — and until then, what is inside is genuinely out of reach. take key will not find it, and neither will has() or here().

An openable container gives up its contents only when it is open; a container with no on open/on close (the basket above) is always transparent. look lists the contents of open containers in the room and examine reports them.

What is in a box is not on the floor, and TAKE says which one it means. take key is the floor and the hand; take key from chest is the box. Every verb that only looks — examine, read, search, use — reaches into an open container as it always did, because seeing into a box is not reaching into it; TAKE is the one that asks the player to say where from:

> open chest
You open the iron chest.
The seam gives with a bang.
Inside the iron chest: brass key, folded note.

> take key
Key is in the iron chest — "take key from iron chest" reaches into it.

> take key from chest
You take the brass key out of the iron chest.
Taken: brass key.

> put key in basket
You put the brass key in the wicker basket.

Any preposition does — take key in chest is the same sentence — and the refusal above names the box the thing is in, so a player who reaches for it the short way is told the long way once and then knows it.

The four things that can go wrong each say so: no such box within reach, a box that holds nothing, a box that is shut, and a box that does not have that in it (There is no "spanner" in the iron chest.).

take all is the floor and take all from <container> is the box, for the same reason and in the same shape: take all lifts what is lying in the room and leaves an open chest alone, and take all from chest empties the chest and leaves the floor alone. A player who wants both says both, and neither can surprise them with the other's things.

> take all from chest
You take the brass key out of the iron chest.
Taken: brass key.
You take the folded note out of the iron chest.
Taken: folded note.

Any preposition will do — take all in chest is the same sentence. A container that is shut, that is empty, that holds nothing the player can lift, or that is not a container at all each says so in its own words, and scenery inside one is passed over in silence exactly as it is on the floor.

put all in <container> is the way back, and it means the hands rather than the floor — which is what all means to drop all too. A player who wants what is lying about in the box says take all first. The container being filled is passed over rather than refused, so carrying the basket you are filling costs nothing:

> put all in basket
You put the brass key in the wicker basket.
You put the folded note in the wicker basket.

put also reads a count, as take and drop do: put 3 arrows in the quiver moves three and leaves the rest, and a container reports what it is holding the way a room does — Inside a quiver: 3 arrows.

here(x) counts an item inside an open container in the room, since the player can reach it. Nesting goes exactly one level deep: a container inside a container is not searched, which keeps take unambiguous and spares everyone a matryoshka puzzle.

Listing an item in two containers, or in both a room and a container, or having two containers hold each other, are all errors. (Listing it in two rooms is not — that is several of it; see Several of a thing.)

Readable things

An item with a text: field responds to read. Because text: is an expression, what a note says can depend on the state of the game:

item note "folded note" {
  examine: "A sheet of paper, folded twice."
  text: if metTheSmith then "Ask the smith for the key." else "The handwriting is unfamiliar."
  on read { set readNote = true }
}

read looks at what you're carrying and what's in the room, so signs and plaques work too — give them fixed: true and a text:.

Searching

search is a built-in command, and declaring on search is what makes an item searchable — the same rule as on read making one readable. An item without the block says so:

room study "The Study" {
  description: "A desk under the window, and a bookshelf that has been reorganised."
  item desk
  item bookshelf
}

item desk "the desk" {
  fixed: true
  on search {
    if not found_letter {
      print "Taped under the middle drawer, where a drawer is not usually taped: a letter."
      spawn letter in room
      set found_letter = true
    } else {
      print "Nothing else. You have already been through it."
    }
  }
}

> search desk
Taped under the middle drawer, where a drawer is not usually taped: a letter.

inspect, rummage and frisk reach the same command. Because the body is ordinary statements, searching the same thing twice can say something different, searching can depend on what the player already knows, and a search can fail informatively rather than silently.

Multi-step discoveries fall straight out of this: have examine mention something odd, and put the real find behind search.

Weapons and armor

A weapon or a piece of armor is an ordinary item with two extra fields. Nothing else marks it out — equip and unequip are offered because the game contains one.

item sword "worn sword" {
  weapon: true
  damage: 3        # added to the player's base attack while equipped
}

item chestplate "steel chestplate" {
  armor: true
  defense: 3        # added to the player's base defense while equipped
}

Putting things in hands: equip and unequip

equipped(itemId) asks whether that item is in a hand or on a back right now. It reads both slots, so it works for a weapon and for armour, and it answers about the item rather than the slot:

item sword "a sword" { weapon: true  damage: 3 }

verb swing {
  if not equipped(sword) { print "Not while it is in your pack." }
  else { print "You swing it." }
}

Carrying is not wielding: has(sword) and equipped(sword) are different questions, and the second is false until the player types EQUIP. Putting the thing down answers no again — a slot holding something you are no longer carrying is not a weapon you are holding.

"Is anything equipped?" is that question over a group, which is the same shape as everything else that walks one:

group arms { sword, axe, spear }

verb ready {
  let i = 0
  let armed = false
  repeat size(arms) { if equipped(arms[i]) { set armed = true }  set i = i + 1 }
  if not armed { print "You are empty-handed." }
}

The name may be spelled out, subscripted, or built from a value, like every other name in the language. Naming an item that is neither weapon: nor armor: is a compile error: it could only ever answer no, which is not a question anybody meant to ask.

equipped is only a call when a ( follows it, so a game may still keep an ordinary flag of that name — see These words are still ordinary names.

equip <item> and unequip <item> are how a game changes what is in those slots:

on enter {
  print "The boards give way, and you go through them to the knee."
  unequip service_pistol
  remove service_pistol from inventory
  print "Whatever was in your hands is at the bottom of the shaft now."
}

They are statements rather than a property to assign, for the reason spawn and remove are: they say a thing happened. Like those, they are silent — the player's own EQUIP prints You equip the sword. (+9 attack), but a game arming or disarming somebody deliberately says so in its own words. For the same reason they run no on equip or on unequip; those blocks answer the player's hands, and a game moving something into a hand already knows what it meant — see On equip and on unequip.

Equipping something the player is not carrying does nothing, the way removing something that is not there does nothing. spawn <item> in inventory first is how a game hands over a weapon it has just invented. An item that is neither a weapon nor armour can never be held, and trying is a compile error rather than a statement that quietly does nothing:

equip lantern
→ zaytunc: item "lantern" is neither a weapon nor armor, so it can never be equipped

Both take the three ways of naming a thing — equip sword, equip arms[i], equip item("blade" + n).

On equip and on unequip

A weapon or a piece of armor can answer being put in hand and taken out again:

item cursed_blade "a blade of black glass" {
  weapon: true
  damage: 9
  aliases: "blade", "sword"
  on equip   { print "The grip is colder than the room. Something settles behind your eyes." }
  on unequip { print "Your hand opens. You had not decided to open it." }
}

They run after the slot has changed, under the line the player already saw — equipped(x) is true inside on equip and false inside on unequip, the same way has(x) is true inside on take. They react to the arming; they do not decide it.

> equip blade
You equip a blade of black glass. (+9 attack)
The grip is colder than the room. Something settles behind your eyes.

Only a weapon or a piece of armor may declare them. Nothing else can ever be in a hand — equip lantern is already a compile error — so on anything else the block would be writing that can never run, and the compiler says so instead of letting it sit there looking like it works.

Equipping into a full slot unequips what was there, and that thing gets its block: there are two hands, one for a weapon and one for armor, so putting a second sword in the first hand takes the first sword out of it. The one that leaves goes first, because that is the order the two things happened in.

> equip axe
You equip a felling axe. (+4 attack)
Your hand opens. You had not decided to open it.

Re-equipping what is already equipped runs nothing: nothing left the hand.

Two things do not run them, and both are the rule on take already follows:

A thing that runs out

uses: is how many times one of a thing can be spent before it needs filling again, and empty: is what it says when it cannot. A weapon spends one on every swing; anything else spends one on every USE:

item pistol "a service pistol" {
  weapon: true
  damage: 14
  uses: 10
  empty: "The pistol clicks. Empty."
  examine: "A service revolver. " + pistol.uses + " left in it."
}

> attack patient
You hit a patient for 17 damage.
...
> attack patient
The pistol clicks. Empty.

At nothing left the swing is refused, not weakened: no blow lands, and nothing answers it — an empty gun costs the turn and not the blood. Reloading is arithmetic, because pistol.uses is one of the item's own numbers:

verb reload { set pistol.uses = 10  print "Ten in, and the cylinder swings shut." }
verb top_up { set pistol.uses = pistol.uses + 6 }     # clamped to what it holds when full

empty: is required with uses:, and refused without it. A thing that can run out has to be able to say so; the alternative is a player who types ATTACK and gets silence.

count: and uses: are different questions. count: 3 is three lockpicks; uses: 3 is one lockpick with three turns left in it. They compose — three picks of three turns each — and a thing that is not a weapon runs out the same way:

item pick "a bent lockpick" {
  uses: 3
  empty: "What is left of it will not turn anything."
  on use { print "The lock gives." }
}

At nothing left the on use block does not run at all.

The hook speaks first. on attack runs before any of this, so a game that wants its own words about an empty gun — or wants one to fire anyway — says them there and empty: never speaks.

A thing's own numbers

An item has damage, defense, count and uses of its own. itemId.damage, itemId.defense and itemId.count read and write them while the game runs:

item rusty_knife "rusty knife" {
  aliases: "knife", "dagger"
  examine: "More rust than knife, but it has a point."
  weapon: true
  damage: 4
}

verb sharpen {
  set rusty_knife.damage = 100
  print "It takes an edge you did not expect."
}

verb grind { set rusty_knife.damage = rusty_knife.damage + 1 }    # it reads, too

Writing is a set like every other write in the language. What is written is the item's from then on, so everything that reads the number sees the new one — the blow it deals, the (+100 attack) an EQUIP announces, STATUS. It is part of the world rather than of the declaration, so it is saved and restored with everything else, and a written 0 is a written number, not "never written".

The compiler knows what the name belongs to, which is the whole reason for the spelling. An item that is not there, or a number an item does not have, is a compile error rather than a branch that quietly never runs:

set nosuch.damage = 1        # refers to undeclared item "nosuch"
set rusty_knife.sharpness = 1  # "sharpness" is not one of an item's own numbers

The older way still works, and is worth knowing because it does something this cannot: damage: takes an expression, so a weapon whose damage is worked out rather than stored — damage: 4 + rage — reads the flag every time it swings. Reach for itemId.damage when the number is the knife's; reach for an expression when it is something else's and the knife only follows it.

A creature has its own numbers too, and the same spelling reaches them: hp, attack and defense. One member of a series says which one, exactly as ehp(mon[2]) does:

set wolf.attack = 1               # something took the fight out of it
set wolf.defense = wolf.defense + 2
set mon[2].hp = 5                 # one of the series, not all of them

creature.hp is not a second way to write hit points — it is the write set ehp(x) = n already was, and goes through the same door, so the rules are the same rather than a second set to keep in step: the value is clamped to what the creature started with, a negative write is nothing rather than a negative number, and crossing to nothing runs on defeat once. Writing 0 to something already beaten does nothing. A creature's live numbers were always part of the world, so these are saved and restored like everything else.

A name worked out while the game runs reaches them too, the same spelling alive, ehp and roomof take:

set enemy("mon#" + i).hp = 6
print item("coin" + n).damage

Which numbers a thing has depends on what it is, and the compiler holds you to it — an item has what it can lend the player, a creature has what it fights with. The kind is written down even when the name is not, so a built name is checked as strictly as a spelled-out one:

set wolf.count = 1               # "count" is not one of a creature's own numbers
set wolf.damage = 1              # ...nor is "damage"
set rusty_knife.hp = 1           # "hp" is not one of an item's own numbers
set enemy("w").damage = 1        # ...and the same, on a name built while the game runs

Several of a thing

An item is normally present or absent. There are two ways to have several of one, and they are the same mechanism written from either end.

Place it more than once. A knife in three rooms is three knives:

room armoury "The Armoury" { item knife }
room kitchen "The Kitchen" { item knife }
room cellar  "The Cellar"  { item knife  item knife }

item knife "a knife" { aliases: "knife" }

One in the armoury, one in the kitchen, two in the cellar. Take them all and you are carrying four knives. Nothing else has to be declared: the count keys on the place as well as the item, so each room keeps its own.

Or declare how many one placement is, which is what you want when the number is the point:

item arrow "an arrow" {
  count: 12
  plural: "arrows"
  aliases: "arrows"
}

The two compose. count: 12 on an item placed in two rooms is a dozen in each.

plural: is optional.

Write one and it is used. Leave it out and the compiler works one out from the title:

item knife "a knife" { }        ->  "2 knives"
item box   "a box"   { }        ->  "2 boxes"
item shard "Shard of Dusk" { }  ->  "2 Shards of Dusk"

It drops a leading a/an/the, pluralises the word before an of rather than the last word, and leaves a title that is already plural alone — "standing stones" stays "standing stones". A singular ending in a single s is the case it gets wrong ("a gas" stays "gas"), and writing plural: is the answer whenever the guess is not what you meant.

The plural is worked out when the game is compiled, not while it runs, and written into the bytecode as though you had typed it. That is deliberate: it is a pile of English special cases, and a rule implemented separately in two runtimes is how this project has produced most of its bugs. So an item that can be several and whose title is an expression is a compile error, since there is no text to read — give that one a plural:.

A container cannot be several of itself. contains: names particular items, and a second copy would have to hold the same ones. Placing a container twice, or giving it a count:, is refused.

A stack can be split. TAKE ARROWS takes all twelve; TAKE 3 ARROWS takes three and leaves nine where they were, so the same item can be a stack of three in your hands and a stack of nine on the floor. Taking the rest merges them back. DROP 5 ARROWS puts five down. Asking for more than there are takes what there is.

A number with nothing after it is not a count: TAKE 1933 asks for a thing called 1933.

Reading and spending:

verb fire {
  if count(arrow) > 0 {
    set count(arrow) = count(arrow) - 1
    print "Thunk. (" + count(arrow) + " left)"
  } else { print "You are out of arrows." }
}

count(x) is how many the player is carrying and count_here(x) how many are in the room — two questions, because a stack can be in both places at once. set count(x) and set count_here(x) write them outright; where a stack is stays spawn, remove, take and drop. count() of an item that is not stackable is 0.

remove takes one of a thing, not the pile. That is what makes the ordinary spelling of spending something work, without the author having to know whether what they wrote turned out to be a stack:

item bandage "a field dressing" {
  on use { heal 30  remove bandage from inventory }
}

Written into eight rooms, that is eight dressings; a player holding three and using one is left holding two. spawn is its opposite and adds one. To take a pile away at once, write the number: set count(bandage) = 0.

Running out removes it. Spend the last arrow and the stack leaves the world: has(arrow) becomes false and nothing lists it, because a stack of none is not something you are carrying.

Counts go into a save and come back. A game with no stacks in it has exactly the save file it always had.

How much the player is carrying

carried is how many things are in the pack; carried_kinds is how many entries it lists. They differ exactly where stacks do:

item arrow "an arrow" { count: 12  plural: "arrows" }

> inventory
You are carrying: 12 arrows, a brass lamp.

carried        -> 13
carried_kinds  -> 2

Which one a game wants depends on what it is counting. A carry limit is usually about entries — a quiver should not fill a pack the way twelve separate things would:

verb take_it {
  if carried_kinds >= 8 { print "Your hands are full." }
  else { ... }
}

Anything to do with weight or bulk is about the things themselves:

if carried > 20 { print "You are slower than you were." }

Both are read-only, and both are counted when they are read rather than kept in a number that something has to remember to update: they follow taking, dropping, spawning, spending from a stack, and a stack running out. size(x) cannot answer this — it is a length known when the game is compiled, and a pack is not.

count(x) and count_here(x) remain the question about one item; these two are the question about the pack.

Time

Every game has a clock whether or not it uses one. This section is the whole of it: what a turn is, what spends one, what runs when one passes, how to spend one yourself, and how to write a game in which none of it shows.

turn is how many have passed. It starts at 0, it reads in any expression, and it is read-only — the engine advances it, and set turn = ... is a compile error. There is no way to set it to a number.

if turn > 40 { finish "The tide comes in." }
description when turn > 30: "The light is failing."

Two statements move it, and neither of them sets it. pass spends turns, which moves it forward; reset turn puts it back to 0, where a new game starts.

verb sleep {
  print "You wake to a grey morning."
  reset turn
}

That is for a story that starts again without the program doing so: a new day, a fresh run of a roguelike, a second attempt at a level meant to be attempted twice. It is not a turn passing, so on turn does not run, nothing ages, no creature strikes, and the world is left exactly where it stood — only the count goes back.

reset is a keyword only in front of turn and elapsed, the way open is only a keyword in front of exit. A game with a verb, an item or a flag called reset is undisturbed, and reset in front of any other word is not a statement at all.

What spends a turn

A turn is spent by a command that acts on the world. Reporting on it is free.

look, examine, read, searcha turn — looking properly takes a moment
take, drop, open, close, put, equip, unequipa turn
movement, and wait (or z)a turn
talk, ask, attacka turn
a verb of your owna turn, unless it says free: — see below
inventory, status, help, topicsfree — they report, they do not act
save, restorefree; they are not moves in the game at all
a command the parser does not recognisefree — a typo is not a move
againwhatever the command it repeats costs
answering a questionfree — see Questions

A command that fails still spends one. Rattling a locked door takes as long as opening it, and pulling at something bolted down takes as long as picking something up. In a game with a deadline this is the difference between a map you can probe for nothing and one you cannot.

The turn is counted after the refusal has spoken, so on turn sees the new number and anything it prints lands underneath:

> north
The door is locked.
(turn 1)

No time passes inside a question. A question is a moment, not a place to loiter in: the answer is free, and so are the few commands that still work while one is open. A creature does not get a swing at somebody who is thinking about a riddle. See Questions.

Making a verb of your own free

A verb you declare costs a turn like any other command. free: says otherwise:

verb score { free: true  print "Score: " + score }

free: is an expression, and it is read after the body has run, so a verb can decide while it runs whether it counted. That is the common case, not an exotic one: a command the game refuses — a shot out of range, a door that is not there — is not something the player did and should not cost them a turn, while the same command doing its job should.

flag fired = false

verb fire {
  free: not fired
  set fired = false
  if aimed < 0      { print "You have not aimed at anything." }
  else if ammo <= 0 { print "The bolt closes on nothing." }
  else              { set fired = true  do resolve_shot() }
}

Reading it after the body is what makes that work: by then the verb knows.

Spending a turn yourself: pass

pass          # one turn of the world's time
pass 2        # two

Since turn is read-only, this is the only way to move the clock. It is a whole turn and not a bigger number: every on turn runs, exactly as if the player had spent it. That is what makes a move which takes longer expressible at all —

verb half_roll { do fly(5) }             # a second
verb wide_turn { do fly(1)  pass 2 }     # rather longer, and the other man gets the time

— and it is on top of what the command already costs, so a verb that costs a turn and passes one costs two.

pass 0, or a count that comes out negative, spends nothing: pass count(shells) with an empty locker is a line that does not happen rather than one that ends the story.

It is bounded. An on turn block that passes a turn runs another on turn, and two of them passing to each other would run until the stack did. Sixteen deep it stops and says (runtime error: turns were passed too many times over; stopping.) — the same answer do, provoke and arriving somewhere give to the same shape of mistake.

on turn: what runs when one passes

Turns only do anything through these blocks. A top-level on turn runs after every turn, which is where anything that ticks lives:

flag fuel = 12

on turn {
  if lampLit {
    set fuel = fuel - 1
    if fuel == 3 { print "The lamp flame shrinks and begins to smoke." }
    if fuel <= 0 { set lampLit = false  print "The lamp gutters out." }
  }
}

It runs after the command it follows has finished, so anything it prints lands under that command's output.

on turn also goes inside a room, an item or an enemy, and then it runs only while that thing is where the player is. This is the usual case — most things that tick, tick in one place — and writing it at the top level means guarding it with a condition about where the player is:

room adrift "Adrift" {
  description: "Open water in every direction."
  on turn {
    set drift = drift + 1
    if drift > 8 { finish "The current takes you out past the shipping lanes." }
  }
}

item bomb "a satchel charge" {
  on turn {
    set fuse = fuse - 1
    if fuse == 0 { hurt 40  print "The satchel goes off." }
  }
}

enemy sentry "the sentry" {
  hp: 20 attack: 4 defense: 2
  on turn { if chance(30) { print "The sentry paces to the far wall and back." } }
}

The rules, all four of them:

Two details that only matter once something moves the player:

The clock on the wall

turn counts the player's moves. elapsed counts their seconds — whole seconds of real time since the game began:

verb score {
  free: true
  print "You have been at this for " + trunc(elapsed / 60) + " minutes."
}

It starts at nought, it never goes backwards, and it is a whole number, so the two runtimes agree without agreeing about milliseconds.

It goes into a save, so a game saved after an hour and restored tomorrow goes on counting from an hour rather than from nought — the same rule the random stream follows, and for the same reason: a save is meant to put the player back where they were.

reset elapsed puts it back to nought, and is the only thing that moves it. It is the mirror of reset turn, for the same reason and with the same shape — back to where a new game starts, never to a number a game chooses, so set elapsed = 60 stays the compile error it has always been.

verb restart {
  print "The level begins again."
  reset turn
  reset elapsed
}

The two clocks are reset separately, because a game may want either without the other: a timed level starting again inside a story that goes on counting its moves is the ordinary case, and so is a new day that resets the moves and leaves the wall clock alone. reset elapsed does not touch turn, reset turn does not touch elapsed, and neither is a turn passing — on turn does not run for either.

What it puts back includes what a restored save brought with it: the hour that game had been open is discarded along with this sitting's seconds, which is what a story starting again wants. A save written afterwards carries nought.

It is the one thing a game reads that a seed: cannot pin. Everything else — the world, the pack, the random stream — is state this language owns and reproduces exactly; a clock is not. Two runs of a game that reads elapsed produce different transcripts, on purpose, and there is no way to make them do otherwise.

That is worth one sentence of advice rather than a warning: read it for the player, not for the game. Printing how long somebody has been playing, timing a speedrun, or telling them it is getting late are all fine. Deciding what happens next by it — a door that only opens after five real minutes, damage that scales with how long you dithered — makes a game that cannot be tested, cannot be walked through, and plays differently on a slow machine. turn is what "later" usually means in a text adventure, and it is reproducible.

A game with no turns in it

Plenty of games have a clock and no use for one: a piece where the point is the reading rather than the solving is not helped by a number that goes up every time you look at something. Nothing has to be switched off — a clock nothing consults is harmless — but three things together make it invisible.

Hide the counter. status { turns: false } takes it off the status line, and if that empties the status completely then STATUS stops being a command at all — see Status.

status { turns: false }

Make your own verbs free, so the ones that only report do not move a clock nobody is watching:

verb think { free: true  print "You turn it over and get no further." }

Write no on turn. That is the real answer, and the other two are tidying. A game with no on turn anywhere has a counter that increments and is read by nothing.

What you cannot do is stop it counting, and there is no reason to want to: turn is still there when a late idea needs it, and a game that never asks cannot tell the difference.

Going the other way — a game that is entirely about its clock — is the same three parts used positively: on turn for what ticks, pass for the moves that take longer than a moment, and comparisons on turn for the deadline.

finish ends the turn where it stands

Nothing after it runs, in that block or in any block waiting on it:

verb boom { print "before"  finish "ENDING"  print "after" }

> boom
before
ENDING

The same holds through a do, an if and a repeat: a proc that finishes does not return to its caller's next statement, a loop stops where it is, and the turn ends. An ending is an ending, so you can write one as an early exit and stop thinking about what follows it:

if money < 0 { finish "The landlord takes the shutters off their hinges." }
if day >= lastDay { finish "The guild's man counts what there is." }
set day = day + 1                 # only reached when neither ending fired

If something has to happen at the end, do it before the finish — the last statement is the one that ends the story, and everything the ending needs comes above it. A block that has to run whatever the outcome belongs in on death or in the branch that decides which ending fired, not after it.

on death is the one block a finish does not cut short. The game is over by the time it starts; the block is what decides how, and an ending is usually written as a line, then a score, then a last word — with the finish partway through:

on death {
  print "*** DEAD ON FLOOR " + depth + " ***"
  finish "You are carried up by people who will not say where."
  print "Monsters killed: " + kills
  print "Gold carried:    " + gold
}

Everything there runs, including whatever the block calls and any repeat inside it. The exemption is the death block and nothing else: a finish anywhere else still unwinds, and it lasts only as long as the block, so a death the player survives — on death { heal 5 }, with no finish in it — leaves the ordinary rule in place afterwards.

A goto into a question stops a block in the same way, but it is a call and not an ending: nothing after it runs, and answering hands the player back to where they were asked. That belongs to questions rather than to the clock, and it is written up in Questions — along with the reason no turn passes while one is waiting.

Randomness

Three expressions, usable anywhere an expression is allowed:

random(6)                 # an integer 1..6, both ends inclusive
random(2, 5)              # an integer 2..5
random()                  # a fraction: 0 or more, and less than 1
chance(25)                # true 25% of the time
oneof("a", "b", "c")      # one of the given expressions, picked at random

random() with nothing in the brackets is the odd one out: it answers a fraction rather than a whole number — nought or more and always less than one. It is the generator's own value, handed over undivided, and every other form here is built out of it.

Reach for it when a whole number is the wrong shape: a probability finer than chance() takes, a weight, a proportion of something.

if random() < 0.03 { print "A gull lands on the rail." }
set drift = drift + random() * wind

chance(3) is the same question as random() < 0.03 and reads better, so use chance where a percentage says what you mean. random() is for the cases where it does not — a one-in-three hundred chance, or a number you are going to multiply by something rather than compare.

All four draw from the same stream, so a seed: pins them together and a save carries them together: a fraction advances the sequence exactly as a whole number does.

oneof evaluates only the branch it picks, and is the quickest way to stop repeated text from reading like a loop:

room woods "Deep Woods" {
  description: oneof("Wind moves through the pines.", "An owl calls somewhere.")
}

Combat damage stays deterministic — the same swing does the same damage every time — so adding these to an existing game changes nothing you didn't ask for. To make a fight swing about, roll it yourself in an on attack block: if chance(30) { print "It lands a wild blow." hurt random(1, 3) }.

The random stream is part of the save. It runs on a small seeded generator whose whole state is one number in the save string, so exporting a save and importing it later resumes exactly where the sequence left off rather than re-rolling. A top-level seed: pins the starting point, which makes a game reproducible run to run — useful while testing, since a bug that depends on a roll stays reproducible. Without seed:, each new game starts from the clock.

Do not set one just to make a test repeatable. That is the obvious use for it and it is the wrong one: a seed: in the source is not a testing aid, it is a decision that every player, for ever meets the same rolls in the same order. A game with wandering sounds, ambush timing and flavour text then hands all of them the identical sequence, and the randomness becomes theatre. Every game in this project once did this, for exactly that reason, and none of them does now.

How the walkthroughs get their repeatable stream instead: they write a seed into the source, build with it, run, and take it out again.

SEED=71534
SRC="$here/deadfall.zaytun"

unpin() {                                   # restores on any exit, including a failed
    [ -f "$SRC.walkthrough-backup" ] || return 0        # assertion or a Ctrl-C
    mv -f "$SRC.walkthrough-backup" "$SRC"
    "$ZC" "$SRC" -o "$BC" >/dev/null 2>&1
}
trap 'unpin' EXIT INT TERM HUP
pin

The seed belongs to that run rather than to the game, which is the distinction worth keeping. Two details make the difference between this working and this losing your source:

Set seed: when the world is meant to be the same for everybody — a puzzle whose layout every player shares, so that a hint is a hint for everybody. That is a statement about the design, and the only reason to write one.

The stream still advances with what the player does, so a pinned seed does not make two different playthroughs alike — it makes the same playthrough repeatable. undergaol is the other extreme: it generates its floor and pins nothing anywhere, so no two descents are alike.

Scenes

Room descriptions cover where you are; a scene covers what is happening to you. It's a named block of narrative paragraphs, and the player renders it set apart from ordinary room text so a cutscene reads as one:

scene opening {
  "Nobody in Emberfall can agree on what the sky used to look like."
  "The old ones say blue, and mean it. Anyone your age has only ever seen the lid."
  ""
  "Eleven years ago a man called Malachar took the Dawnstone and broke it over his knee."
}

intro: opening

intro: names the scene shown once at startup, after the title and before the first room — the place to put the story so far. Any other scene is played from a statement, so cutscenes can land wherever the story wants them:

on defeat { play the_kings_last_words }
on enter  { if not warned { play the_warning  set warned = true } }

A blank line is just an empty paragraph, "".

Paragraphs are expressions, not string literals, which is what makes a scene worth having over a run of prints — narration can read the state of the game and change accordingly:

scene summit {
  "There is a moment on the last stair where you can still turn round."
  if has(sunsteel_blade)
    then "The Sunsteel Blade is warm against your back."
    else "You have a farm sword and a shield somebody's aunt made you."
  "You climb the last stair."
}

Scenes hold paragraphs and nothing else — no statements, and therefore no play — so one scene can never trigger another and the construct cannot recurse. Naming a scene that doesn't exist, in intro: or play, is a compile error.

Verbs

The built-in commands cover moving, handling objects and fighting. Anything else — pray, sing, dig, listen, shout — you declare yourself, rather than faking it as use on a scenery item you would have to remember to place in every room the player might try it in.

A complete, compilable example — a chapel with three verbs of its own:

title: "Vespers"
start: chapel

flag blessed = false
flag songs = 0

verb pray {
  aliases: "kneel", "worship"
  if here(altar) {
    if blessed { print "You have already been heard once. Twice would be greedy." }
    else {
      print "You kneel at the altar. Something enormous turns over, and hears you."
      set blessed = true
    }
  } else {
    print "You kneel on bare stone. Nothing is listening in here."
  }
}

verb sing {
  set songs = songs + 1
  if noun == "" { print "You sing, wordlessly." }
  else { print "You sing about " + noun + ". The acoustics are excellent." }
}

verb score {
  free: true
  print "Songs: " + songs + ".  Blessed: " + (if blessed then "yes" else "no") + "."
}

room chapel "The Chapel" {
  description: "A cold chapel with a stone altar. The yard is north."
  exit north -> yard
  item altar
}

room yard "The Yard" {
  description: "Wet grass and no roof."
  exit south -> chapel
}

item altar "stone altar" { fixed: true  examine: "Older than the chapel around it." }

Three things that example is showing:

The body is an ordinary statement block, so a verb decides for itself what it means in the room the player is standing in. pray reads here(altar) and gives a different answer in the yard. You write the verb once; you do not place a prayable object anywhere.

noun holds whatever the player typed after the verb, lowercased, or "" if they typed the verb alone. sing and sing a lament for the drowned both reach the same block.

free: true stops a verb costing a turn — right for anything that reports rather than acts, like score above. Without it, a verb advances the clock like any other action.

It is an expression, and it is read after the body has run, so a verb can decide while it runs whether it counted. That is the commonest case there is: a command the game refuses — a shot out of range, a door that is not there — is not something the player did, and should not cost them a turn, while the same command doing its job should.

flag fired = false

verb fire {
  free: not fired
  set fired = false
  if aimed < 0        { print "You have not aimed at anything." }
  else if ammo <= 0   { print "The bolt closes on nothing." }
  else                { set fired = true  do resolve_shot() }
}

Reading it after the body is what makes that work: by then the verb knows. Written the other way round there would be nothing to read yet, and free: would have to be decided before the verb had seen the noun.

A verb with no free: at all costs a turn, as it always did.

aliases: adds more words that reach the same verb. help lists a game's own verbs underneath the built-in ones, so players can find them.

A verb of your own owns its word

A verb you declare wins over the built-in command of the same name. status, take, look, help, the compass, save, again — every word the player's command layer answers to is yours to take, and when you take one, whatever it meant before is gone from your game:

verb status {
  free: true
  print "HULL " + hull + "   FUEL " + fuel + "   CREW " + crew
}

verb take {
  print "You do not take things here. You ask, and you are usually refused."
}

That is the whole rule, and it holds everywhere a word is decided:

Aliases count. verb fence { aliases: "take", "get" } takes all three words.

Two words are not yours: quit and exit. They are read by the program, above the story entirely, so a verb of either name would compile and then never run — which is the silent failure this rule exists to prevent, not an exception carved out of it. There is always a way to stop playing.

verb quit { print "You cannot leave." }
→ zaytunc: "quit" leaves the program rather than the story, so a verb of that name would
  never run — there is always a way to stop playing

And two verbs still cannot share a word, whoever the word belonged to before: one of them would be shadowed and you could not see which.

Taking a word is a decision, not an accident. A game that redefines take has to answer every place the player might reach for it, because the built-in is not there to fall back on. Take a word when the game genuinely means something else by it — a ship's status, a court's ask, a séance's talk — and pick a fresh word when it does not.

Two objects

A command is VERB [OBJECT [PREPOSITION OBJECT]], so a verb can take an indirect object. Three values are available inside a verb body:

nounthe first object, as typed ("" if the verb was typed alone)
prepthe preposition that separated them ("" if there wasn't one)
otherthe second object, as typed

in into inside on onto at to with from under behind over against about for through are recognised as prepositions.

Reading other is what gives your verb a second object. A verb that never mentions it — nor other means x, nor a proc that reads it — has nowhere to put a second object, and the whole tail of the line arrives in noun however many prepositions are inside it: poke man in black gives noun the four words rather than the first one. A verb that does read it keeps the two slots, so TURN DIAL TO 7 still arrives as dial and 7. Nothing is declared; the compiler works it out from the verb's own text. See A preposition inside a name is part of the name.

Comparing raw text is fragile, so two expressions resolve what the player typed against a real item they can reach, aliases and all — noun_is(itemId) and other_is(itemId). "Can reach" is load-bearing and is spelled out below:

verb unlock {
  if not noun_is(iron_door) { print "There is nothing like that to unlock." }
  else {
    if prep != "with" { print "Unlock it with what?" }
    else {
      if other_is(brass_key) { print "The key turns."  set doorOpen = true }
      else { print "The " + other + " is not a key." }
    }
  }
}

Typing unlock the door with the key then prints "The key turns." — give coin to beggar, throw rock at window and tie rope to hook all work the same way. Use noun_is/other_is when you mean an item and the raw noun/other strings when you don't: an NPC's name, a direction, a word in a song.

Leading articles are ignored when resolving an item, so take the lantern and take lantern are the same command.

They only match something the player can actually reach. Both resolve against the same pool TAKE and EXAMINE use — what the player is carrying, what is in the room, and one level inside an open container in either place. An item somewhere else does not match, however exactly the player typed its name:

room study "The Study" {
  description: "A desk, and a door west."
  exit west -> hall
  item ledger
}
room hall "The Hall" { description: "Bare."  exit east -> study }
item ledger "the day book" { aliases: "ledger", "day book" }

verb show { free: true  if noun_is(ledger) { print "matched" } else { print "no match" } }

show the ledger prints matched in the study and no match in the hall — and no match in the study too if the player has not picked it up and the room is dark, because an unlit room puts nothing but your own hands within reach. This is deliberate: noun_is(x) answers did the player name x, and could they touch it, which is nearly always the question a verb is asking. A game that lets present the ledger to the doctor succeed while the ledger is still in a room the player has never entered has given the puzzle away.

If you want the two halves separately, ask separately: noun_is(x) for the naming, here(x) or has(x) for the whereabouts. To know only whether the player typed a particular word, and not whether it named a real thing at all, compare the raw noun string.

noun_is_npc(id) and other_is_npc(id) ask the same question about a person, resolving through that character's id, display name and aliases. Reach works the same way it does for items — only people in the room can be named:

verb present {
  if prep != "to" { print "Present what, and to whom?" }
  else {
    if noun_is(ledger) and other_is_npc(baker) {
      print "The baker looks at the ledger for a long moment."
      set baker_rattled = true
    } else { print "That means nothing to them." }
  }
}

verb accuse {
  if noun_is_npc(baker) { print "You say it out loud. The room goes quiet." }
  else { print "There is nobody by that name here." }
}

present the ledger to the baker, accuse Mrs Cobb and show the photograph to the doctor all work without the author ever comparing raw strings. This is what a game built on confronting people with evidence needs, and it is why noun_is alone was not enough.

noun_is_enemy(id) and other_is_enemy(id) ask it about a creature, and complete the set — one for each kind of thing a room can hold. They go through the resolver ATTACK goes through, so what a creature answers to cannot come to mean two things, and the reach rule arrives with it: a creature can be named while it is alive, in the player's room, and the room is lit.

enemy raider[4] rname[member] { aliases: rname[member]  hp: 14  attack: 5  defense: 0 }

verb aim {
  let t = -1
  let i = 0
  repeat size(raider) {
    if noun_is_enemy(raider[i]) { set t = i }
    set i = i + 1
  }
  if t < 0 { print "Nothing out there answers to \"" + noun + "\"." }
  else { print "You bring the sights onto " + rname[t] + "." }
}

aim the runner, aim runner and aim raider0 all reach the same creature, and one that has been beaten or has not been spawned reaches none. Before this, a game with a verb of its own had to compare noun against strings by hand — which cannot see an alias, cannot strip an article, and cannot tell a creature in the room from one on the far side of the map.

All three take the same three ways of naming a thing: spelled out, subscripted (raider[i]), or built from a value (enemy("mon" + i)).

Reading a number the player typed

tonumber(noun) and tonumber(other) read the raw text the player typed as a number, and isnumber(noun) / isnumber(other) ask whether it is one. This is how a game takes a number the player thought of rather than one it offered:

verb turn {
  aliases: "set"
  if not noun_is(dial) { print "Turn what?" }
  else if not isnumber(other) { print "\"" + other + "\" is not a year." }
  else if tonumber(other) < 1912 { print "The dial will not go back that far." }
  else {
    set year = tonumber(other)
    print "The dial settles on " + year + "."
  }
}

> turn the year dial to 1933
The dial settles on 1933.

Without it, a dial like that has to cycle through presets the author chose — a different puzzle, and a smaller one, because the player picks from a menu instead of having an idea.

What counts as a number: optional surrounding blanks, an optional single + or -, digits, and a decimal part if there is one. So 1933, +1940, -50, 000123, 1.5 and -2.75 are numbers; 19x3, 1e3, 0x10, 12 monkeys, 3., .5 and an empty slot are not. The rule is narrower than what C or JavaScript accept on their own, deliberately: the two disagree — strtod reads 1e3 and stops happily at the x in 19x3, Number() reads 0x10 — and both runtimes have to give the same answer.

It is the same rule the engine already used to do arithmetic with text. "3.0" / 2 has always been 1.5, because a string a game divides is read as the number it is written as; tonumber had a narrower reader of its own, so tonumber("3.0") was 0 and isnumber("3.0") was false. One engine cannot read the same text two ways, and it no longer does.

Text written with a point reads as a real, exactly as a literal does — so tonumber("3.0") / 2 is 1.5 while tonumber("3") / 2 is 1, and isint(tonumber("3.0")) is false. The spelling decides, which is the rule Two kinds of number already sets out.

tonumber returns 0 for text that is not a number, and 0 is also a perfectly good number. Use isnumber to tell them apart rather than reading 0 as failure — and check it first, because that is also where the message for bad input belongs.

The argument is any expression, read as text and then as a number — so the same two questions reach a flag, a proc's parameter, or a string you built:

proc classify(w) {
  if isnumber(w) { return tonumber(w) }
  return -1
}

verb turn   { print "" + classify(other) }
question q "Q" { description: "?"  on answer { print "" + classify(reply) } }

They took noun, other and reply and nothing else, on the argument that these should not be pointed at a value nobody typed. What that cost was every routine: a proc could not be handed a word to classify, so the logic that reads what a player typed had to be written out once against noun for a verb and again against reply for a question, and the two kept in step by hand.

The three slots are ordinary expressions and read exactly as they did. reply is still readable only inside a question's on answer — that rule belongs to reply and applies here because the argument is walked like any other expression, which is also why a misspelt name inside one is now a compile error.

Help

help only lists commands the game can actually use. The compiler works out what a game contains and records it, and the runtimes build the list from that:

commandoffered when
take drop inventorysome item is not fixed:
usesome item has on use
open closesome item has on open or on close
readsome item has text: or on read
searchsome item has on search
put <item> in <container>some item declares contains:
talk askthe game has an npc in it
attackthe game has an enemy in it
equip unequipsome item is a weapon: or armor:
go <direction>some room declares an exit

look, examine, wait, save and restore are always offered. So is status, unless the game has emptied it — see Status.

A hull's own words are listed with steer. A vehicle that says steer: "sail", "cast off" has taught this game two words, so HELP names them beside the engine's own entry, in the order the game declares them and each one once:

steer <direction> (or sail, cast off)

LOOK names them too, on the hull's own line — but only while the player is aboard her, and a player who has not boarded anything yet has nowhere else to find them. A hull that names no words of her own leaves the entry as it is.

Movement is offered only where there is somewhere to go. A game whose rooms declare no exit at all moves the player with goto and nothing else — a fight, a quiz, an interrogation, a hand of cards — and telling that player about north, south, east, west, up and down is telling them six words that do nothing in it. One exit anywhere in the game brings the compass back.

A game with one usable item and one person to talk to:

item lamp "a lamp" { aliases: "lamp"  on use { print "It clicks on." } }
npc guard "The Guard" { dialogue: "Hm." }
room hall "The Hall" { description: "A hall."  item lamp  npc guard }

> help
Commands: look, examine <thing>, go <direction> (or north/south/east/west/up/down),
take <item>, drop <item>, inventory, use <item>, talk <name>, ask <name> about <topic>,
wait, status, again, save [file], restore [file].

No attack, because nothing in it can be fought; no open, because nothing opens; no read, search or put, for the same reason. The compass is there because the hall has a way out; take the exits away and it goes too. The commands still work if typed — north in a game with no exits still answers You can't go north from here. This decides what is advertised, not what exists.

So a detective game with no combat does not advertise attack, and a game with no doors does not advertise open. There is nothing to configure and nothing to keep in sync — it follows from what you declared.

A hidden command still works if the player types it. This changes what is advertised, not what is accepted, so a game that spawns its only takeable item at runtime is not broken by it; it simply will not suggest take until there is something to take.

Inferred hiding is advisory; declared hiding is absolute. The rule above holds whenever the compiler worked out that a command has nothing to act on, because that inference is deliberately conservative and can be wrong. Where the author has declared that something should not exist — status { ... } is where that is said — it does not exist, and typing it gets the ordinary "I don't understand". The difference is that a guess should not lock a door, and a decision should.

A verb that collides with a built-in command or one of its synonyms is a compile error, and so are two verbs claiming the same word — either way the verb would simply never fire, and a silent no-op is worse than a build failure.

Hiding a command from HELP

Everything above is worked out when the game is compiled. hide … from help and show … in help are the same decision made while the game runs, and they reach the game's own verbs as well as HELP's commands:

hide take from help            # HELP stops offering it
show take in help              # ...and offers it again

verb learn_to_drive { ...  show drive in help }   # a verb the player is not told about until now

The statement says where, and the compiler insists on it. hide and show are ordinary game words — three of the games in this project alias an item to one of them — and a bare show take reads like revealing something to the player rather than editing a list of commands. Naming HELP says what the statement does and leaves both words in your vocabulary: a flag called hide, an item called show and a verb show of your own all keep working.

hide take                      → zaytunc: "hide" says where it is taking it from: hide take from help
show take from help            → zaytunc: "show" says where it is putting it back: show take in help

The word is written out, the way shuffle order names a flag: hide court from help is the command COURT, not the value of anything called court. To name one while the game runs, write an expression — the brackets are what tells the two apart:

hide (whichever) from help
hide (flag("cmd")) from help

It changes what HELP says and nothing else. A hidden command still works when it is typed, which is the same rule inferred hiding follows — so hide is for a game that does not want to advertise something yet, not for a game that wants to forbid it. What a player may do is decided by the verb's own body, and a verb that should refuse should say so in its own words.

show is an override. A command the compiler would have left out — open in a game with nothing openable — appears when the game says show open in help, because the author may be about to spawn the thing it acts on. A switch that only worked one way round would be one you could not undo.

Three states, not two. Told to hide, told to show, and never mentioned — and the last is every game that says nothing, which goes on following the table above exactly as it did.

The word has to be one HELP could print: one of its own commands, or one of this game's verbs. A name written out is checked when the game is compiled; one worked out while it runs is reported when it happens, and the game carries on:

hide nosuch from help
→ zaytunc: HELP has nothing called "nosuch" to show or hide — name one of its own commands, or one of this game's verbs

hide (w)      # w holds "nosuch" at play time
→ (runtime error: HELP has nothing called "nosuch" to show or hide.)

quit is not on the list. There is always a way to stop playing, which is why a verb may not be called QUIT either; taking it out of HELP is the same thing done more quietly, and it is refused in the same words.

What has been hidden and shown is part of the save, so a game resumed says what it said when the player put it down.

Status

STATUS — also STATS and HEALTH — is the player's line on their own condition. It prints only the numbers this game actually has, which is the same principle HELP follows: a game says what is true of it and nothing else.

At its fullest it is four lines:

> status
HP: 12/20  |  Attack: 7 (base 3)  |  Defense: 2 (base 0)
Score: 5 of 100
Equipped — weapon: a sword, armor: a coat of mail  |  Turns: 14

...and in a game with nothing to fight, wear or score, one:

> status
Turns: 14

Which lines appear

Every one of these is worked out by the compiler from what the game declares. There is nothing to turn on — only, where the guess is wrong for your game, something to turn off.

lineshown when
HP: x/ythe game has an enemy, or moves the player's health anywhere in it — a hurt, a heal, or a plain set hp = ...
Attack: and Defense:the game has an enemy in it
Score:the game sets score: or maxscore:, or writes score anywhere — see Score
a stat's own lineany stat was given a label: — see Custom stats
weapon:some item is a weapon:
armor:some item is armor:
Turns:unless hidden — see below

They are printed in that order, and Turns: shares a line with Equipped — when there is one and stands alone when there is not.

Printing HP: 20/20 | Attack: 3 | Defense: 0 in a detective story would invite the player to plan around three numbers that can never move, and imply that somewhere there is a sword to raise the third — a promise the author never made. A game with a hurt but no enemies is a real case, and shows HP without the combat numbers behind it: health can drop, but there is nothing to attack and nothing to attack you.

Hiding a line: status { ... }

The rules above are a good guess and they are sometimes wrong, because what a game declares and what it means are not always the same thing. A game that works out its own damage still declares an enemy, so it is a fighting game as far as the compiler is concerned, and it was told

HP: 60/60  |  Attack: 0 (base 0)  |  Defense: 0 (base 0)

for ever — two numbers that never move, in a game where all the arithmetic happens somewhere else.

status { ... } says which lines not to show. Every field defaults to shown; naming one false takes it off the status line:

status {
  attack: false
  defense: false
}

> status
HP: 60/60
Turns: 14

The fields are hp, attack, defense, score, equipped and turns. equipped covers both the weapon and the armor, since they share a line.

Hidden is hidden, not disabled. The numbers go on existing and go on changing: hp still falls when something hits you, score still adds up, and both still read in any expression. This is a sentence about the status line and nothing else — a blow still reports (14/20 HP) in its own message, because that is the blow talking, not STATUS.

Hide every line and STATUS stops being a command, exactly as it already does for a game that never had a status line — it answers I don't understand "status" rather than printing a blank, and HELP stops offering it.

A stat you gave a label: is not in the list, because it does not need to be: the label is both the player's word for the number and the request to print it. Take the label away and the stat stops being shown, which is the same sentence said in the place it belongs.

There was once a turns: false at the top level, from when the move counter was the only line anybody wanted rid of. One idea spelled two ways — one field outside the block and five inside it — is worse than either spelling on its own, so it is now the sixth field and the old form is refused with the line to write instead:

zaytunc: "turns:" is now one line of the status block -- write "status { turns: false }"

A rename that changes nothing and says nothing is worse than a break; this one says so.

Score

A game can keep a running total for discoveries and milestones. score and maxscore are reserved identifiers like hp and maxhp: readable in any expression, and writable with set. There is no separate statement for awarding points, because there does not need to be one.

player { score: 0  maxscore: 1000 }

room vault "The Vault" {
  description: "The door stands open at last."
  on enter { if not looted { set score = score + 50  set looted = true } }
}

STATUS then reports it, with the target if the game named one — see Status for the rest of that line and the ones around it:

Score: 75 of 1000

A score is opt-in, and a game without one is unaffected. Declaring either field turns it on; so does writing to score anywhere, so a game can award points without declaring anything. A game that does neither has no score at all and its STATUS is exactly what it was — this is not a line every game gains.

A score is held to maxscore, and only there. Once a game states a target, that is the ceiling: set score = score + 50 on a game at 380 of 400 leaves 400, the way a stat is held to its own max and hp to maxhp. Lowering the target pulls the score down with it; raising it leaves the score where it is. A game that states no target has no ceiling and counts as far as it likes — maxscore absent is the same "no ceiling" a stat with only a start: has.

player { score: 0  maxscore: 400 }

> score
You have 400 of 400 points.        # ...and not 504 of 400

The floor stays open, which is the half that was always right: a penalty may take a score below zero, and a game may mean that. Negative hit points are not a thing a game can mean, which is why hp is clamped at both ends and a score at one.

Starting above the target — player { score: 500 maxscore: 100 } — is a compile error, for the reason a stat starting above its max is: it would be pulled down by the first write, so it could never be the number that was asked for.

There is no built-in SCORE command, and that is deliberate: score is a perfectly good name for a verb of your own, several games already use it, and claiming it here would turn those into compile errors. Write the verb you want:

verb score {
  free: true
  print "You have " + score + " of " + maxscore + " points."
}

Attack: 7 (base 3)

The first number is what the player actually strikes with, equipment included; the one in brackets is their own. The pair is there because the difference is the whole point of picking a sword up — a single number could not show that the sword is what is doing it. Defense: reads the same way.

Both are the attack and defense an expression reads, and neither can be written: see The engine's own names under Variables.

The Equipped line

Equipped — weapon: a sword, armor: a coat of mail  |  Turns: 14
Equipped — weapon: none  |  Turns: 3

Each half appears only if the game has something of that kind to hold: a game with weapons and no armour gets the weapon half alone. none means the slot is empty, not that the game has no weapons — which is the distinction worth having, since it tells the player there is a slot to fill.

It never names something the player has already put down. Dropping a wielded sword empties the slot, and STATUS says so on the next line it prints.

Stats, if you name them for the player

A stat appears here when it has a label:, under that label, in declaration order:

stat hunger { max: 100  start: 60  label: "Appetite" }
stat depth  { start: 3  label: "Depth" }                # no ceiling — see "Custom stats"
stat cooldown { max: 3 }                                # no label, not shown

> status
HP: 12/20  |  Attack: 7 (base 3)  |  Defense: 0 (base 0)
Appetite: 60/100  |  Depth: 3
Equipped — weapon: a sword  |  Turns: 2

A stat with a ceiling reads as a fraction of it; one without reads as the number alone, because Depth: 3/0 would be a claim about a maximum that does not exist.

The label is both halves of the decision: what the player should call the number, and whether they should see it at all. There is deliberately no separate "show this" — a stat cannot reach the screen without the author choosing a word for it, because the name in the source is a name for the program. A game that calls a stat hunger may well want the player to read Appetite, and one that calls it a while working something out wants the player to read nothing.

label: is an expression like every other field, so a game with two languages can decide it while it runs. An empty one is a compile error: an empty heading is not a heading, and leaving the field off is how a stat stays yours.

What STATUS does not report

For anything that wants a sentence rather than a number — "Health 18/18, appetite 100/100." — a verb is still the answer, and still more capable than a line:

verb diagnose {
  free: true
  print "Health " + hp + "/" + maxhp + ", appetite " + hunger + "/" + maxhunger + "."
}

Write free: true on it. Without it, looking at your own condition costs a turn — which advances every clock in the game, including the one that is making you hungry.

It costs no turn

STATUS is free: the counter does not advance, no on turn runs, and nothing in the world moves because the player looked at their own numbers. HELP and INVENTORY are free for the same reason — see Time.

Inside a question it is not a command at all. Every line the player types at a question is an answer, and status is one of them; the six words that stay commands there do not include it — see Questions.

Turning the move counter off

The counter is one of the six lines you can decide yourself:

status { turns: false }

A move counter is true of every game — time passes whether or not the author thought about it — so there is no way to infer that a game does not want one. Some do not: a piece where the point is the reading, not the solving, is not helped by a number that goes up every time you look at something.

It hides the counter and nothing else. The game still counts turns: turn reads them, on turn still runs, and a deadline written as if turn > 40 is unaffected. Only the display changes.

If that empties the status entirely, status stops being a command. It leaves HELP, and typing it gets the ordinary "I don't understand" rather than a blank reply — a command that answers with nothing reads as broken, and the player types it again. So in a game with no combat, no equipment, nothing that changes health and the counter hidden, there is no status:

> status
I don't understand "status". Type "help" for a list of commands.

In a game that still has something to report, the counter goes and the rest stays:

> status
HP: 12/20  |  Attack: 4 (base 3)  |  Defense: 1 (base 0)

turns: takes true or false and nothing else; turns: 3 is a compile error rather than a quiet false. Saying turns: true is the default and changes nothing.

Procedures

A proc is a named statement block, invoked with do. It exists for the pattern that a large game repeats in thirty places:

flag clue_count = 0
flag clue_text = ""

proc note_clue {
  set clue_count = clue_count + 1
  print "*** Noted: " + clue_text + " ***"
  print "(" + clue_count + " things worth writing down so far.)"
}

item torn_page "a torn page" {
  on take {
    set clue_text = "a page torn from the parish register"
    do note_clue
  }
}

Set the flags it reads, then do it. That is the whole mechanism — deliberately small, so that a proc is easy to reason about and cannot quietly change control flow.

Or give it parameters, which is usually clearer than setting a flag first:

proc note_clue(text) {
  set clue_count = clue_count + 1
  print "*** Noted: " + text + " ***"
}

on take { do note_clue("a page torn from the parish register") }

A parameter is a name bound for the length of one call and nowhere else. Reading it outside its own proc is a compile error, and passing the wrong number of arguments is too. A parameter may shadow a flag of the same name — that is the author's business — but not a name the runtime owns, so proc p(hp) is refused.

Answering: return

return leaves a proc. On its own it is an early exit; with an expression it is an answer, and a proc that answers can be written wherever a value can:

proc bigger(a, b) {
  if a > b { return a }
  return b
}

set best = bigger(score, high_score)
print "The best of them was " + bigger(3, 9) + "."

The value is on the same line as the word, which is how a reader tells too: return n * 2 answers; a return with the next statement on the line below does not.

proc note(text) {
  if text == "" { return }              # nothing worth writing down
  set clue_count = clue_count + 1
  print "*** Noted: " + text + " ***"
}

return unwinds the way finish does — out of an if, out of a repeat, out of the block that ran it — but it stops at the proc it belongs to. A bare return used as an early exit ends its own proc and nothing above it, so the do that called it carries on with the next statement.

A proc that never answers cannot be used as a value. Writing one where a value belongs is a compile error rather than a silent 0:

proc note_clue(text) { set clue_count = clue_count + 1 }
set n = note_clue("x")
→ zaytunc: "note_clue" never returns a value, so it cannot be used as one —
  give it a "return <expression>", or call it with "do"

A proc that can answer but reaches the end of a path without doing so answers 0, which is what every unset thing in the language reads as. A proc that answers may still be called with do, and its answer is then discarded.

Procs are depth-capped at 32. A proc that calls itself deeper than that stops with (runtime error: a proc called itself too many times; stopping.) rather than taking the stack with it. That is the same idea as the 10,000-pass cap on repeat, with a much smaller number, because a proc 32 deep is already a mistake where repeat 100 is not.

Each call gets its own parameters, so a proc that calls itself does not overwrite the values it was called with:

proc countdown(n) {
  if n > 0 { print n  do countdown(n - 1) }
}

A proc may call another proc. Recursion is capped: a proc that reaches a depth of 32 stops and says so, because a game that recurses forever should tell you rather than take the runtime down with it. Naming a proc that was never declared is a compile error, like every other cross-reference in the language.

Arrays

Six ways to have several things under one name, and one subscript that reaches into all of them. This handbook calls each of them by what it is — slots, a table, a flag the game grows, a list, a group, a series — and this section is all six in one place.

Which one you have decides what x[i] means, and the declaration is what decides it:

declared asone of them ishow many
slots on a flagflag price[3] = 0price[i]length(price)
a tableflag bond[4][4] = 0bond[a][b] — two subscripts, alwaysrows(bond) and columns(bond)
a flag the game growsflag cast[] = ""cast[i], of however many it haslength(cast)
a listflag hand = []hand[i], or at(hand, i)length(hand)
a groupgroup stock { sword, lamp }stock[i] — an item, a person, a creature or a vehiclelength(stock)
a seriesenemy rat[8] "a rat" { ... }rat[i] — one of the eight creatureslength(rat)
...or of vehiclesvehicle lifeboat[6] "..." { ... }lifeboat[i] — one of the six — see Vehicleslength(lifeboat)

Three rules hold for all six. Subscripts count from 0, so price[0] is the first and price[3] is one past the end of three. A fractional subscript truncates, as a repeat count does. And a subscript that reaches nothing answers 0 and says so, rather than ending the game:

(runtime error: price has slots 0 to 2, so [7] names nothing.)

The first three are a flag's own shape, fixed when the game is compiled or grown while it runs, and they hold values. A list is a value — it goes in a flag, into a proc and back out of one, and its length is whatever it grew to. A group and a series name things you declared: a group is an order over items, people or creatures written out one by one; a series is one description stamped out several times.

Which to reach for:

you wantreach for
one number or string per something, and you know how manyslots
a fact about a pair — what A thinks of Ba table
as many as the game turns out to needa flag it grows, or a list
something to hand to a proc and get backa list
eight rats, each with its own hit points and its own rooma series
six lifeboats, each with its own position and its own contentsa series of vehicles
an order over three things that are all differenta group

Everything after this is those six in turn, and then the vocabulary they share: subscripts on a value, names worked out while the game runs, and the four ways of counting.

Slots on a flag

A flag can hold several values under one name:

flag chamber_kind[8]
flag chamber_name[8] = "Cell"

That is eight of each, written and read with a subscript:

set chamber_kind[3] = 2
print chamber_name[3]

A length can be worked out. [8] is a number; it is also any sum the compiler can do for itself, which is what stops the count drifting from whatever it counts:

group quiz { q_capital, q_river, q_year }
flag order[size(quiz)] = 0                  # as long as the quiz, and it cannot fall behind

enemy patient[7] "patient " + member { ... }
enemy hound[4]   "a hound" { ... }
enemy nurse[3]   "the ward sister" { ... }
flag alert[size(patient) + size(hound) + size(nurse)] = 0     # fourteen, and it stays fourteen

It takes numbers, size(...) and + - * / % over them, and nothing else — a length has to be knowable with the file in front of you, so a flag's value cannot be one:

flag n = 4
flag p[n] = 0
→ zaytunc: this cannot say how many — a length is worked out when the game is compiled...

A series' length is written the same way — enemy rat[size(pack)] — and two lengths that ask each other (flag a[size(b)] beside flag b[size(a)]) are refused rather than chased.

A starting value is optional here and required on a plain flag, which is not the inconsistency it looks like: a plain flag's value is the declaration — flag lit = false is the sentence "the lamp starts unlit" — while slots are almost always filled in before anything reads them. Left off, every slot starts at 0. Write it when the starting state matters, as chamber_name does above.

Subscripts start at 0, so flag chamber_kind[8] has chamber_kind[0] through chamber_kind[7], and chamber_kind[8] is one past the end. They are ordinary expressions — which is the point. Together with a proc parameter, one body can serve every slot instead of being written out once per slot:

proc rebuild(i) {
  set chamber_kind[i] = random(1, 6)
  set chamber_name[i] = oneof("Bone Pit", "Sump", "Rope Room")
}

room c4 chamber_name[3] {
  on enter { do rebuild(3) }
}

The compiler checks the shape of a subscript and not its value: a flag with slots cannot be read without one, and a flag without slots cannot be written through one. Whether an index names anything is the runtime's to answer, and it answers it out loud:

(chamber_kind has slots 0 to 7, so [8] names nothing.)
(stock is a series of 2, so [7] names nothing.)
(q is 3 characters long, so [6] names nothing.)
(hand is a list of 2, so [7] names nothing.)

Nothing stops and no value changes. A read outside gives 0 — or "" where a character was asked for — and a write outside does nothing, the same spirit as 1 / 0 being 0 and a runaway repeat stopping rather than ending the story over an off-by-one. A fractional subscript truncates, as a fractional repeat count does.

An index is never a reason to refuse a program. chamber_kind[8] on a flag of eight is the mistake counting from zero invites, and it used to be a compile error — but refusing it made the index a restriction on what could be written rather than a report of what happened, and a line that never runs is not a mistake anybody made. So it compiles, and says so if it runs. That is one rule at one moment for every index there is: slots, groups, creature and person series, the characters of a string, the items of a list.

at() says the same thing, because it names one item as a subscript does. substr() does not, because it is a range: the end of the text is a legal place to start, and asking for more than is left gives what is left. A start past the end is reported; a negative start is not, because a range that begins before the text begins at the text —

substr("abc", 0 - 2, 2)   # "ab": the range starts at 0
substr("abc", 6, 1)       # "" and a report: there is no sixth character to start at

— so substr and at are what to reach for when you are probing rather than indexing, and length() is usually better than either.

One number, not six. size() reads a flag's slot count, and a slot count may be written as any sum the compiler can do — so a set of tables that must stay the same length says so once:

flag qtext[110]          = ""
flag qmodel[size(qtext)] = ""
flag qhint[size(qtext)]  = ""

Each is a number in the bytecode exactly as 110 would be, and none of them can fall out of step with the others.

shuffle <flag> deals the slots. It fills them with 0 up to one less than how many there are, in a random order — which is how a game draws without repeating, since a shuffle has no repeats in it to begin with:

flag order[100] = 0

verb begin { free: true  shuffle order  set put = 0 }
proc ask_another { set put = put + 1  goto quiz[order[put - 1]] }

The alternative, written by hand, is a probe: pick at random, remember what has been picked, and step past it next time. That works and it is four lines longer, and the length has to agree with a repeat bound and a % in two more places.

It draws from the game's own random stream, so a seed: pins the order like everything else, and both players deal the same cards. The flag's slot count is the length — there is nothing else it could be — and a flag without slots is a compile error, since there would be nothing in it to shuffle.

Slots go into a save file as a list and come back the same way. A game that uses none has exactly the save file it always had.

Putting slots in order

sort <flag> is shuffle's mirror. Where shuffle fills the slots with a random order, this puts what is already in them into an ascending one:

flag score[4] = 0
...
sort score                  # -12, 0, 40, 40

sort <flag> by <other> is the one a table wants. It fills the flag with 0 up to one less than how many there are — a drawing order, exactly as shuffle does — arranged so that reading the other flag through it goes low to high:

flag price[4] = 0
flag order[4] = 0

sort order by price
let i = 0
repeat 4 { print padright(good[order[i]], 8) + padleft("" + price[order[i]], 4)
           set i = i + 1 }

Without it a table can be shown in no order but the one it was declared in, because nothing rearranges the names and the prices together. Sorting an order over them leaves both alone.

One order, and it is total. Every number comes before every piece of text; numbers compare as numbers, text compares by character, and a boolean is a number. A list compares as the text it prints as. That is spelled out rather than left to each platform, so the two runtimes arrange the same values the same way.

It is stable. Two equal keys keep the order they were declared in — which is what makes a table of prices readable when three things cost the same.

The flag's slot count is the length, as shuffle's is; a flag without slots is a compile error, and so is a table, for shuffle's reason: what "in order" means across rows and columns has more than one answer.

A flag the game grows

A length written [] is a length the game decides:

flag cast[] = ""            # no slots yet
flag tie[][] = 50           # ...and a square table, no rows and no columns yet

grow adds one, holding whatever the declaration said a slot starts at:

proc join(name) {
  grow cast
  set cast[size(cast) - 1] = name
}

Everything else about slots is unchanged: the same subscript, the same write, the same report when one reaches nothing, the same place in a save file. What changes is that size() is answered while the game runs rather than folded to a number — it is the count the flag has now.

repeat size(cast) { ... }

A table the game grows is square, and grow adds a row and a column. That is not a restriction dressed up as a feature: a growing table is a fact about a pair drawn from one growing set — who gets on with whom, who has met whom — and a set has one length. rows() and columns() both answer it, and both are read at play time:

flag tie[][] = 50           # tie[a][b], as wide as the cast is long

proc join(name) {
  grow cast
  grow tie                  # the new person's row and column, at 50 with everybody
  set cast[size(cast) - 1] = name
}

The slots are one flat run, so growing the table re-lays them: the stride between rows is the number of columns, so widening it moves every row but the first. What was in it stays where it belongs — tie[0][1] is what it was before the fourth person arrived.

A flag with a declared length cannot be grown, and a growable one cannot be a length:

flag price[4] = 0
grow price
→ zaytunc: "price" was declared with a length, so it is that many for ever — declare it as "flag price[] = ..." for a flag the game grows

flag seat[size(cast)] = ""
→ zaytunc: "cast" is as long as the game decides, so it cannot say how many — a length is worked out when the game is compiled

Both halves of that are the same rule seen twice. flag order[size(quiz)] is a number in the bytecode, and a number cannot come from a count that does not exist yet.

The ceiling is 10,000 slots, and it is not the 1000 a declared length is held to. That number is a guard against a typo — a hand-written flag c[5000] is nearly always a mistake — and nothing here is typed: a game that has grown a flag ten thousand times meant to. It is still a ceiling, because every slot is written into every save file, and reaching it says so and carries on, as every other limit here does:

(runtime error: cast has 10000 slots, which is as many as a flag may have.)
(runtime error: tie has 100 rows of 100, which is as many slots as a flag may have.)

A growable table stops at 100 rows of 100, which is the same 10,000 slots.

How many it has grown to is part of the save. It is the one count the game itself does not know: everything else keeps the length it was declared with and fills what it can, while this comes back as long as it was.

Which to reach for. A declared length says this many, by design — one per station, one per member of a group — and the compiler checks the arithmetic around it. [] says as many as the game turns out to need, and the cost is that nothing about the count can be checked before it runs. A list is the third thing and a different one: it is a value, so it can be handed to a proc and answered from one, which slots cannot — but its items cannot be written one at a time. Reach for [] when a game accumulates records — people, ships, entries in a ledger — and for a list when a game passes a handful of values around.

Rows and columns

A flag can have two lengths instead of one:

flag bond[4][4] = 0

That is a table: four rows of four, sixteen values under one name, reached by two subscripts.

set bond[2][3] = 5
print bond[2][3]

Everything slots already do, a table does. The lengths may be worked out — flag bond[size(cast)] [size(cast)] is as wide as the cast and cannot fall behind it. A starting value is optional and every slot begins with it. The values go into a save file and come back. The subscripts are ordinary expressions, so one proc can serve every pair.

What it is for is a fact about two things rather than one. Slots hold a fact about each of several: price[i] is the price of good i. A table holds a fact about each pair: what Anna thinks of Bob is neither a fact about Anna nor a fact about Bob.

flag name[3]      = ""
flag bond[3][3]   = 0        # bond[a][b] is what a thinks of b

proc meet(a, b, warmth) {
  set bond[a][b] = bond[a][b] + warmth
  set bond[b][a] = bond[b][a] + warmth
}

Without it, that is one flag per person — bondToAnna[3], bondToBob[3], bondToCara[3] — three declarations to say one thing, and a fourth person means a fourth declaration and every proc that walks them rewritten.

rows() and columns() are the two halves of the shape, so a loop is written against the declaration rather than against a number copied out of it:

flag a = 0
flag b = 0

proc clear {
  set a = 0
  repeat rows(bond) {
    set b = 0
    repeat columns(bond) { set bond[a][b] = 0  set b = b + 1 }
    set a = a + 1
  }
}

Both are worked out when the game is compiled, exactly as size() is, so neither costs anything at play time — and both may be used as a length, which is how a second table is kept the same shape as the first: flag met[rows(bond)][columns(bond)] = false.

size() is refused on a table, and rows() and columns() are refused on a flag that has one row. One number cannot say what two numbers say, and picking one of them silently is the sort of quiet wrongness this language spends its checks on:

print size(bond)
→ zaytunc: "bond" has rows and columns, so size() cannot say how many — ask for rows(bond) or columns(bond)

print rows(price)
→ zaytunc: "price" has 4 slots in one row, so size(price) is how many it holds

How many subscripts a name takes is checked; what they reach is not. The shape is written in the declaration, so the compiler knows it and says so:

print bond[1]
→ zaytunc: "bond" has 3 rows of 3, so it needs two subscripts: "bond[0][0]"

A subscript's value is the runtime's to report, in the words every other index uses:

(runtime error: bond has rows 0 to 2 and columns 0 to 2, so [0][3] names nothing.)

That report is doing more work than it looks. A table's slots are one flat run — row 1 of a 3-wide table starts at slot 3 — so bond[0][3] is, in arithmetic alone, bond[1][0]: a real slot holding a real number, which would answer about the wrong pair and never say a word. Both subscripts are checked before that arithmetic is done, on both runtimes, which is why a column past the end of its row reads 0 and reports, rather than quietly reading its neighbour.

shuffle is refused on a table. It fills slots with a drawing order, and what "in a random order" means across rows and columns has more than one answer, so it is not guessed at: shuffle a flag of one row and use it to reach the table.

The limit is 1000 slots however they are shaped, since every one of them is written into every save file: flag big[40][40] is 1600 and is refused, in those words.

A second subscript on a flag of one row still means what it always did — one item of what that slot holds:

flag word[3] = "ali"
print word[0][1]      # "l": the second character of slot 0

Which of the two readings a second subscript has is settled by the declaration, the same way one subscript already means a slot on chamber_kind[3] and a character on q[3]. Neither runtime learns that a table was ever spelled any other way.

Deeper than a table

A declaration says at most two lengths. A value has no such limit: a slot can hold a list, and a list can hold lists, and a subscript past the flag's own shape reaches into what is there.

flag sector[8][8] = []          # a table of lists: two lengths declared, three subscripts

set sector[3][4] = ["...", "*..", "..K"]
set sector[3][4][2] = "..."     # the third line of that quadrant's map
print sector[3][4][0]

The declaration accounts for the first subscripts — none on a plain flag, one on a row, two on a table — and every subscript after that belongs to the value. Nothing has been added to the declaration: sector is the same 8×8 table it was, and what makes it three-deep is that its slots hold lists.

Reading has always allowed this. What is new is writing. sector[3][4][2] was a perfectly good expression while the only way to change it was to rebuild the whole list and assign it back — so a game that wanted a third dimension either did that on every write, or gave up and rebuilt the map from scratch each time it was needed. A write now reaches as deep as a read.

Where the shape ends is decided by the declaration, and nowhere else. On flag row[4] = 0, row[1][0] is the first item of the list in slot 1; on flag bond[4][4] = 0, bond[1][0] is the pair. The same two subscripts, meaning different things, because the two names were declared differently — which is the rule a second subscript has always followed.

What a subscript reaches is still reported when it reaches nothing, in the words every other index uses, and the write does not happen:

(runtime error: sector is a list of 3, so [7] names nothing.)
(runtime error: word is text, not a list, so [1] cannot be written.)

Writing one character of a piece of text is the second of those. Text is not changed in place anywhere in this language: build the new value with replace() or substr() and assign it.

A list is still copied and never shared. A deep write changes the flag it names and nothing else, even where a copy of the list was handed out earlier:

set hand = sector[3][4]
set sector[3][4][0] = "KKK"
print hand[0]                   # what it was: the copy is a copy

What it is for is a shape a declaration cannot name. An eight-by-eight galaxy whose quadrants each hold an eight-by-eight map is four numbers deep; the alternative to keeping it is working it out again on every arrival, which is a different game — the quadrant no longer remembers where its stars were when you last visited.

Lists

A flag holds one thing: a number, a string or a boolean. A list holds several, under one name, and is a value like any other — it goes in a flag, into a proc and back out of one.

flag accepted = []                      # the empty list; nothing else spells it
flag suits = ["hearts", "spades"]       # a flag may start as one

set accepted = split("a cat|the cat|cat", "|")
if reply in accepted { print "Yes." }
[a, b, c]a list. Any expression inside, and [] is the empty one
x in listis x one of them?
length(list)how many — the same word that counts a string's characters
at(list, i)the i-th, counting from 0 — and it says so when what it is handed is not a list
append(list, x)a new list with x on the end
split(s, sep)a string cut into a list
join(list, sep)...and back into a string

Nothing changes a value in place. append answers a new list and leaves the old one alone, so a list grows the way a number does:

set hand = append(hand, "the ace")

A subscript writes one item of a list a flag holds — set hand[0] = "the king", see Deeper than a table — and that is the same rule, not an exception to it: the flag's own item changes and every copy of that list which was handed out earlier is untouched.

A list reads as its items with a comma between them, which is what a game printing one nearly always wants: print hand says the ace, the two. join is there for any other separator.

at outside the list is 0, the answer a subscript outside a flag's slots gives and the answer 1 / 0 gives — a game does not end over an index — and it is reported, because at names one item. A fraction truncates, as a subscript does.

Two lists are equal when they hold the same things in the same order. split("a|b", "|") == split("a|b", "|") is true: a list is compared by what is in it, not by which one it is.

An empty list is false, exactly as an empty string is, so if hand { ... } asks whether there is anything in it.

split("", "|")        # no items at all, not one empty item
split("abc", "")      # "a", "b", "c" -- an empty separator cuts every character

A list and a flag's slots are different things

Both hold several values and they are not the same tool. Slots are a fixed-width table declared with the flag — flag price[3] is three numbers, for ever, and price[i] is one of them. A list is a value: its length is whatever it grew to, and it can be handed to a proc and answered from one, which slots cannot.

Reach for slots when the count is part of the design — one per station, one per member of a group. Reach for a list when the count is whatever the game put in it.

A slot can hold a list, and a list can hold a list. flag qans[110] where every slot is a list of accepted answers is a table of sets, and it is written the way it reads:

set qans[i] = split(model + "|" + alts, "|")

A group: naming an order over things you already declared

A series is a mould — one description stamped out eight times. That is exactly what eight rats are, since each needs its own hit points and its own place, and no count can express that.

A shop's stock is the opposite. A sword is not a lamp, and stamping one description three times gives three things the player cannot tell apart or name. For that, declare them properly and then say what order they are in:

item sword "a bronze sword" { aliases: "sword" }
item lamp  "a brass lamp"   { aliases: "lamp" }
item rope  "a coil of rope" { aliases: "rope"  count: 20  plural: "coils of rope" }

group stock { sword, lamp, rope }

stock[0] is sword — the whole item, with its own title, its own aliases, its own count:. A group declares nothing of its own; it names an order over declarations that already exist. So every form that takes a subscript works, and the prices can live in a flag's slots beside it:

flag price[3] = 0

verb unbar { let i = 0  repeat size(stock) { spawn stock[i] in room  set i = i + 1 } }
verb buy   { let i = 0
             repeat size(stock) { if noun_is(stock[i]) { spawn stock[i] in inventory }
                                  set i = i + 1 } }

let binds the counter in the block around the repeat, so it carries from one pass to the next, and size(stock) cannot drift from the group it counts. Walking a group with a proc that calls itself works too, but it counts down — a shop stocked that way stocks its last item first — and procs are depth-capped at 32, so a group larger than that is walked partway and abandoned with (runtime error: a proc called itself too many times; stopping.) in the middle of it. Keep recursion for a proc that genuinely calls itself on a smaller problem.

Because every member is written out, every name in a group is checked when the game is compiled, exactly as it was before. Nothing is built from a value at play time.

The rules:

Which to reach for. If the members are interchangeable and differ only in where they are, a series is shorter. If the player will ever name one of them, or they differ in any way at all, they have to be real declarations — so use a group.

A series: one description, several creatures

enemy rat[8] "a rat" { hp: 4 } declares eight rats, addressed rat[0] to rat[7], each with its own hit points and its own place in the world. npc guard[3] and item torch[4] do the same for people and things.

A series is indexed exactly as a flag's slots are, and counted the same way — length(rat), size(rat) — which is why it is on the map at the top of this section. Everything else about it belongs with the thing it declares, so it is documented there: see A series of creatures under Enemies, and Declaring people once, and placing them under People.

A series is not a group. A series is one description stamped out; every member is the same thing with its own state. A group is an order over things declared separately, each of which can be entirely different. See A group: naming an order over things you already declared just above.

A name built from a value

Three of the ways to say which thing is meant are checked before a byte is emitted. The fourth is not, and is written differently so that a reader can tell:

lantern              spelled out
stock[i]             the i-th of a group or a series
item("coin" + n)     a name worked out while the game runs
group(which)[i]      ...and the i-th of a GROUP worked out while the game runs

The last one works wherever a name does — item(...), room(...), enemy(...), npc(...) and flag(...):

spawn item("coin" + n) in inventory
goto room("cell" + depth)
move npc("guard" + i) to room("post" + i)
if alive(enemy("mon" + i)) { ... }
set flag("c" + i + "_kind") = 2
exit north -> room("cell" + nextCell)

group(<expr>)[i] names one of a group the game chose, which is what a flag holding "technical" or "science" is for. Without it the choice has to be a branch per group, and the flag holding the name does nothing:

if bank == "technical" { goto technical[i] }        # a branch per bank...
else if bank == "science" { goto science[i] }
else { goto history[i] }

goto group(bank)[i]                                 # ...or the lookup it was always meant to be

It works where a place is named — goto and an exit's target — and it goes with size(group(<expr>)), which is the one size() the compiler cannot fold into a number, because the name is not known until the game runs:

let n = size(group(bank))
let i = random(0, n - 1)
goto group(bank)[i]

Both miss the way every name built from a value misses: a group nobody declared has a size() of 0, and an index past the end goes nowhere and the block carries on.

It is places only. has(group(kind)[0]) is not written — a thing chosen while the game runs is item(<expr>), which reaches any item by name, and a group adds nothing there that a name does not already give.

What it costs. Every other name in the language is checked when the game is compiled, so a misspelling is a compile error rather than a branch that quietly never runs. A name built from a value cannot be checked that way — so the check happens while the game runs instead, and says so:

> fetch
(runtime error: no item is called "coin9".)

The command stops making that particular change and the game carries on, the same way a runaway repeat reports and stops. The kind you wrote is the kind that has to be found: item("hall") naming a room is reported, not quietly accepted. has_flag(<expr>) asks whether a flag name reaches anything without being told off for it.

Use a spelled-out name or a group where you can, and this where you cannot — but it is a real form, not a workaround, and the error it produces is a normal part of running a game.

An array can be named this way too, with the subscript after the brackets:

flag dist[40] = 0
flag ldist[40] = 0

proc fill(field, from) {
  let i = 0
  repeat 40 { set flag(field)[i] = 99  set i = i + 1 }
  set flag(field)[from] = 0
}

do fill("dist", playerroom_index)
do fill("ldist", gramophone_index)

This is what lets one proc work on either of two arrays: the array is an argument. Without it the proc has to be written twice, once per array name, and the two copies have to be kept in step by hand for as long as the game exists. A subscript past the end reads as 0 and a write there does nothing, exactly as a spelled-out subscript does.

An index is a value too

A subscript is also a way to hold which thing in a variable, and it stays checked at compile time, so it is usually the better of the two:

flag chosen = 0
flag kind[4] = ""
group weapons { sword, axe, bow, pike }

verb ready {
  let i = 0
  repeat size(weapons) { if noun_is(weapons[i]) { set chosen = i }  set i = i + 1 }
  print "You take " + kind[chosen] + "."
}

chosen is an ordinary number. weapons[chosen] is the item it stands for, and kind[chosen] is state kept alongside it. Both are checked when the game is compiled, because weapons and kind are written out.

Note what this is not for: which weapon the player is actually holding is equipped(weapons[i]), and asking the engine beats keeping a second copy of the answer — a flag set by a verb goes stale the moment the player types UNEQUIP. Reach for an index when you need something the engine does not already know, such as which of four the player last asked for.

size(x)

How many a group, a series, or a flag's slots hold:

size(weapons)   size(mob)   size(crew)   size(kind)

The reason it exists is that a loop bound must not drift from the declaration it matches. Written by hand, do pick(2) goes on compiling after a fourth weapon joins the group and silently stops visiting the last one — a wrong answer with no error, which is the one thing this language is built to avoid.

Every one of those lengths is known when the game is compiled, so size(x) is replaced by the number: do pick(size(weapons) - 1) produces exactly the bytecode do pick(4 - 1) produces, and no runtime knows the word. It is a keyword only where it is a call, so a game that already has a flag called size keeps it.

length(x) answers the same question about anything, and is the word to reach for when you are not thinking about which kind of name you have:

length(weapons)     # 4 — a group, exactly as size() says
length(crew)        # a flag's slots, however many it has been grown to
length(hand)        # the items in a list a flag holds
length(noun)        # the characters of a piece of text

size asks the declaration and length asks the value, and where both make sense they agree. Keeping the two words is worth one sentence of explanation: size(x) refuses a name that was never declared with slots, which catches "I thought this had some" at compile time —

flag bits = 0
print size(bits)
→ zaytunc: "bits" is one flag, so it has no slots to count — length(bits) is how many its
  value holds, or declare it as "flag bits[n] = ..." for slots

— while length(x) answers whatever is there. A table is the one shape neither word can count, because two numbers cannot be one: ask for rows(t) or columns(t).

A map whose shape is data

An exit's target is worked out when the player walks through it, not when the game is compiled. So a flag's slots and room(<expr>) together are a map that can be rearranged in play:

flag nextCell[3] = 0

room c0 "Cell 0" { description: "..."  exit north -> room("c" + nextCell[0]) }
room c1 "Cell 1" { description: "..."  exit north -> room("c" + nextCell[1]) }
room c2 "Cell 2" { description: "..."  exit north -> room("c" + nextCell[2]) }

verb turn { set nextCell[0] = 2  set nextCell[1] = 0  set nextCell[2] = 1 }

Before turn, every cell's north leads to cell 0; after it, the ring is wound the other way. A name that reaches no room is reported and the way out refuses, rather than moving the player nowhere.

A passage that opens and closes is the condition doing the same job. Put the two together and a room's whole map is data:

flag openD[4] = false
flag destD[4] = 0

room cell "A Cell" { description: "Bare stone."
  exit north -> room("r" + destD[0]) if openD[0]
  exit south -> room("r" + destD[1]) if openD[1]
  exit east  -> room("r" + destD[2]) if openD[2]
  exit west  -> room("r" + destD[3]) if openD[3]
}

proc carve(d, to) { set openD[d] = true  set destD[d] = to }
proc seal(d)      { set openD[d] = false }

carve(0, 1) opens north onto room 1; seal(0) shuts it again. A shut direction is not listed in Exits: at all and refuses with your own else message if the player tries it, so a room can gain and lose ways out while the game runs without any of them being written twice.

Control

Four things decide whether a block runs, how many times, and which of two values a line ends up with: if, repeat, while, and the if expression — with break and continue for leaving a loop early. Everything else in the language is declarations and statements that always run.

There is deliberately no for each; what to write instead has its own section below.

Branching

if hp < 5        { print "You can barely stand." }
else if hp < 12  { print "You are hurt." }
else if hp < 20  { print "You are winded." }
else             { print "You are unhurt." }

else if is a chain, and it is exactly the nested form written flat. These two compile to the same bytes — literally the same file — so it is a matter of how the source reads and nothing else:

if a { ... } else if b { ... } else { ... }

if a { ... } else { if b { ... } else { ... } }

The nested spelling indents once per branch and closes all of them at the end, so four conditions end with } } } } and the fifth is four levels deep. That is where mistakes live. A chain stays at one level however long it gets.

The chain is ordered: the first condition that is true wins and the rest are not evaluated. A trailing else is optional, and without one a chain that matches nothing simply does nothing.

Conditions as values

if is also an expression. The statement chooses which block runs; the expression chooses which of two values a line ends up with, and can go anywhere a value can:

if <cond> then <expr> else <expr>

The else is not optional: an expression has to produce something either way, and leaving it off is expected "else". A chain works the way it does in the statement form — if a then x else if b then y else z — which is nesting written flat, exactly as else if is.

It is what makes a field that is written once read differently later:

description: if alive(goblin) then "A goblin blocks the way." else "The bridge is clear."
ambush: if angered then "The guardian swings again." else "The guardian's eyes open."
print "Blessed: " + (if blessed then "yes" else "no") + "."

Parenthesise it when it sits inside a larger expression, as in the print above. The else branch runs to the end of the expression, so this

print "x:" + if b then "Y" else "Z" + "!"

reads the + "!" as part of the "Z" and prints x:Y — the exclamation mark quietly disappears whenever the condition is true.

Between this and description when, prefer description when for a room whose description depends on state — it is a list of cases rather than a nest — and reach for the expression where the value is short or the field has no when form. See Conditional descriptions.

Repetition

Two loops. repeat runs a block a number of times; while runs it for as long as a condition holds.

verb toll {
  repeat 3 { print "The bell tolls." }
}

verb flood {
  repeat rising_rate { hurt 1  set water = water + 1 }
}

verb bail {
  while water > 0 and stamina > 0 {
    set water = water - bucket
    set stamina = stamina - 1
  }
}

repeat evaluates its count once, before the first pass. A body that changes it does not extend its own loop — repeat rate { set rate = rate + 10 } with rate at 2 runs twice, not forever. A count of zero or less runs the body no times, a fractional count truncates so repeat 3.7 runs three times, and loops nest.

while reads its condition afresh before every pass, which is the whole difference: the body can move the goalposts, and a loop whose end is a question rather than a count is written with it. A condition false at the start runs the body no times.

Which to reach for: if you can say how many times before the loop starts, repeat says so in the code and cannot be wrong about it. If the end is a state — until the boat is empty, until the crowd disperses, until the search finds what it is looking for — that is while.

A program that runs away is stopped and says so, rather than freezing the terminal or the browser tab:

(runtime error: execution limit exceeded (possible infinite loop); stopping.)

That limit is not a property of loops. It is a step budget the interpreter spends as it walks: every expression it works out and every statement it runs costs one step, and so does every pass a loop makes. When the budget is gone, whatever was running stops.

It is worth knowing three things about it:

Unbounded recursion is a different fault and has its own answer — (a proc called itself too many times; stopping.) — because a stack that cannot grow is not the same problem as a program that will not finish. See Procedures.

Leaving a loop early

break leaves the innermost loop it is written in. continue ends this pass and starts the next.

verb search {
  let i = 0
  let found = 0 - 1
  repeat length(cast) {
    if alive(cast[i]) { set found = i  break }
    set i = i + 1
  }
  ...
}

Without break that loop has to carry a flag — if found < 0 and ... — tested on every remaining pass, and go on walking a list it has already finished with. The flag version is not slower in any way a player would notice; it is just code that says something other than what it means.

continue is the other half, for the pass you want to skip rather than the loop you want to leave:

let i = 0
repeat length(row) {
  set i = i + 1
  if row[i - 1] == "" { continue }      # nothing here; next
  print "  " + row[i - 1]
}

Both belong to the innermost loop written around them, and to nothing else. They do not cross a proc: a proc called from inside a loop cannot break it, for the same reason a return in that proc does not end the caller. A proc's own loops and its own breaks are its own business.

Outside a loop, both are refused when the game is compiled:

verb v { break }
→ zaytunc: "break" leaves a loop, and there is no loop here

That is the whole of the safety argument for them: a break that is not inside a loop is not a runtime case to define, it is a program the compiler will not write down.

Neither word is reserved elsewhere. A flag, an item or a verb called break is untouched — the words are statements only where a statement can begin, and everywhere else they are ordinary names.

while is not the loop a fuse wants, and there is no for each

while exists — see Repetition — and it is the right form for a loop that ends on a state inside one command. It is the wrong form for the thing people most often reach for it to write, which is a loop that runs across turns while the player does other things. A fuse burning down is not a while; it is an on turn and a counter:

flag fuse = 3
flag lit = false

room cell "The Cell" {
  description: "A cell."
  on turn {
    if lit {
      set fuse = fuse - 1
      if fuse > 0 { print "The fuse burns down. (" + fuse + ")" }
      else { finish "The charge goes off. THE END." }
    }
  }
}

> light        You light the fuse.
               The fuse burns down. (2)
> wait         Time passes.
               The fuse burns down. (1)
> wait         Time passes.
               The charge goes off. THE END.

The loop is the game's own turn. Written as a while inside light, the whole fuse would burn in the instant the player struck the match and they would never get to run.

There is no for each, and there is no need for one: a group, a series and a flag's slots are all counted by length(), so a walk over a collection is a counter and a repeat.

let i = 0
repeat length(stock) { spawn stock[i] in room  set i = i + 1 }

A name for the rest of the block

let names a value for the block it is written in and no longer — the counter a repeat binds is one — and set then writes whichever binding is in scope. Both live with the other variables; see Local values.

Where else a condition appears

A condition is not only an if. The same expression decides these, each documented where it belongs, and knowing they exist saves writing an if around something that has its own form:

description when cond: "..."a room's description, by case — see Conditional descriptions
dark if condwhether the room can be seen at all — see Darkness
exit north -> hall if cond else ...whether a way out is open — see Exits
item lamp if lit then "..." else "..."a name that changes — see Items
retaliates: condwhether a creature hits back — see Enemies
ambush: if ... then ... else ...which line it strikes with — see Enemies
while cond { ... }a loop that ends on a state, inside one command — see Repetition
on turn { if cond { ... } }the ordinary way to mean "every turn until" — see Time

Readable spellings

Some things have a second spelling that reads as English. Most of them are questions — carrying lamp for has(lamp) — and the last three are statements. Nothing here is a new question or a new statement: each spelling compiles to exactly the bytes its first form compiles to, so which you write is a matter of how the line reads and nothing else.

function or statementreads as
has(lamp)carrying lamp
not has(lamp)not carrying lamp
alive(goblin)goblin is alive
not alive(goblin)goblin is dead
isopen(chest)chest is open
not isopen(chest)chest is shut
equipped(sword)sword is equipped
npcin(guard, hall)guard is in hall
noun_is(lamp)noun means lamp
noun_is_npc(baker)noun means baker
noun_is_enemy(wolf)noun means wolf
other_is(lamp) and its twoother means lamp
exitopen(hall, "north")north is passable from hall
not exitopen(hall, "north")north is barred from hall
has_flag("c" + n)there is a flag called "c" + n
not has_flag("c" + n)there is no flag called "c" + n
isnumber(reply)reply is a number
not isnumber(reply)reply is not a number
contains(noun, "rope")noun contains "rope"
r == "y" or r == "yes"r in ("y", "yes")
provoke goblinmake goblin attack
goto shrinego to shrine
refusethe blow does not land

in is the one that saves a repetition rather than a function call. A question with several right answers is written once instead of once per answer, and the thing being asked about is named once instead of once per rung:

if reply in ("yes", "y", "aye", "go on") { ... }

Any number of alternatives, each an ordinary expression — n in (m, m + 1, m * 2) is fine, and a misspelt name inside one is caught where any other misspelt name is. in () with nothing in it is refused, because it can only ever be false.

It becomes the chain, so the left-hand side is evaluated once per alternative. f() in (1, 2) calls f twice, exactly as f() == 1 or f() == 2 does. That is rarely worth a thought, but when the left-hand side is a proc that costs something or changes something, put a let in front of it — which is what you would have done writing the chain out by hand:

let answer = tidy(reply)
if answer in ("a cat", "the cat") { ... }

in is a keyword only where a keyword can go — after an expression and before a ( — so a game may still have a flag called in, and if in in (1, 2, 3) means what it says.

is dead and is shut are there because they are the negations that actually get written — of the twenty-four alive() in the five shipped games, twelve are not alive(...). is barred, there is no flag and is not a number are there for the same reason.

means rather than is, and one word for all three kinds. noun is lamp would read as string equality, and noun == "lamp" is a different question: it compares the raw text, where this resolves through the thing's id, its title, its title without the article it carries, and its aliases — and only reaches what the player could touch. The one spelling covers an item, a person and a creature, because the kind is settled by the name: an author should not have to know which kind of thing they are asking about before they can ask, which is the argument X is here already makes. Naming something that is not one of the three — a room, a question, a scene — is a compile error rather than an answer of no.

A subscript works as it does anywhere: mon[2] is dead, carrying arrow[i].

X is here, which is the one that is not a respelling

lamp is here          # an item
guard is here         # a person
goblin is here        # a creature

This is a question the language could not ask. here(x) is for things and npchere(x) is for people — two names, two shapes, and an author had to know which kind something was before they could ask where it is. A creature had neither: alive(goblin) is true from the other side of the map.

One spelling asks about all three. The name is looked up when the game runs, so:

Note the last one, because it is the one that surprises: goblin is dead and goblin is here are both false once it is beaten. is here is about the room; is alive is about the world.

These words are still ordinary names

A word is a keyword only where a keyword can go, so flag here = 0, flag carrying = 0, flag go = 0 and the rest are all still legal, and so are flag has, flag alive, flag isopen and flag count — the older predicates read a following ( before deciding, which they did not always do. put, take and send are not keywords at all any more, so they are as free as any other word.

flag here = 0
flag carrying = false

verb tally {
  free: true
  set here = here + 1
  print "counted " + here + ", lamp here: " + here(lamp) + ", carrying it: " + carrying lamp
}

> tally        counted 1, lamp here: true, carrying it: false
> take lamp    Taken: a lamp.
> tally        counted 2, lamp here: false, carrying it: true

here is a flag and here(lamp) is the predicate, on the same line, and neither is confused with the other. So is carrying, beside carrying lamp.

is is the exception. It is the only one that appears after a name, so a + is cannot be told from the start of a is here, and a flag may not be called is.

The statements that have none, and why. spawn, remove, move, equip, unequip, heal, hurt, finish and shuffle have no second spelling, and it is not an oversight: each is already an English sentence. spawn coin in room and move guard to hall say what they do in the words you would have chosen, and a second way to write a line you can already read is a word taken out of the author's vocabulary for nothing. goto, refuse and provoke are the three that were not already English — goto is the one piece of programmer's jargon in the language, refuse never says what is being refused, and provoke goblin does not say which way round it goes, where make goblin attack does.

Strings

A string is written between double quotes and may contain four escapes, and only these four:

\na line break
\ta tab
\\a backslash
\"a double quote, which is how dialogue is written inside a description

Anything else after a backslash is a compile error rather than a guess:

zaytunc: game.zaytun:14: "\d" is not an escape; the escapes are \n \t \\ \"

That matters more than it looks. Until these four were made explicit, a backslash before anything except n produced the character itself and dropped the backslash — so "\d" was d, and a description written with a tab in it was quietly wrong rather than refused.

The string functions

length(s)              contains(s, t)
upper(s)  lower(s)     substr(s, start, count)
trim(s)                replace(s, from, to)
padright(s, n)         padleft(s, n)
<expr>[i]              one character of it

length is the one of these that is not only about text: it counts the items of a list, and the slots of a group, a series or a flag that has them — see size(x).

A character is a Unicode code point, not a byte and not a UTF-16 unit. length("Lúthien") is 7, length("—") is 1, and substr cuts on character boundaries — so a name with an accent in it behaves the same in a terminal as in a browser, which counting bytes in one and units in the other would not.

upper and lower change ASCII only: upper("Lúthien") is "LúTHIEN". Beyond ASCII the two runtimes' libraries disagree about what a capital letter is, and one of them depends on the machine's locale, so anything outside ASCII is passed through untouched rather than converted one way here and another there.

A source file is UTF-8, and the compiler holds it to that. A byte that is not part of a valid character is refused with the line it is on — an overlong encoding, a half of a surrogate pair, a character that stops early or one past the last there is. Nothing downstream could tell such a byte from a name: the terminal would hand it to the terminal and the browser would replace it with U+FFFD, and the same game would print two different things.

padright(s, n) and padleft(s, n) put spaces round text until it is n characters wide, so a table lines up. padright leaves the text against the left margin and padleft against the right, which is what a column of numbers wants:

print padright(name, 12) + padleft("" + price, 5)

sugar          40
rum            12
hemp            7

Text already that wide or wider comes back untouched — they pad, they never cut, and substr is what cuts. A width of nought or less pads nothing. They count characters, as everything here does, so a column of accented names lines up rather than drifting by a byte a name.

substr counts from 0, and clamps rather than failing: a start past the end gives "", asking for more than is left gives what is left, a negative start is the beginning and a negative count is none. There is no negative indexing.

It is a range, so the end of the text is a legal place to start — substr(s, length(s), 1) is the empty tail — and a count larger than what is left is not out of range. A start outside the text is, and is reported: (that is 3 characters long, so there is nothing at 6.). Nothing stops and the answer is still "".

replace(s, from, to) changes every one of them, left to right, and never looks again at what it has just put down — so replacing "a" with "aa" ends rather than running away. from is text and not a pattern: a full stop in it means a full stop. An empty from matches nothing and leaves the string alone.

A subscript reads one character, so a string can be walked the way a list is:

flag q = "ali"
print q[1]                       # l

let i = 0
repeat length(q) { print upper(q[i])  set i = i + 1 }

It goes on any value, not only on a name. A literal, a call, a bracketed sum — and it chains, because a word of a sentence is a string like any other:

print "ali"[1]                   # l
print upper(q)[0]                # A
print split(line, " ")[0][0]     # the first letter of the first word
print noun[0]                    # the first letter of what the player typed

It is substr(q, i, 1) written shorter, and it follows substr's rules about characters and not bytes, and a fraction truncating. An index outside the string is "" — a character that is not there is no characters — and it says so, because a subscript asks for one particular character and there is no such one. substr is the range and stays quiet; length(q) is how a loop knows where to stop.

The same subscript on a flag WITH slots still means its slots. That is the one place the brackets mean something else, and the compiler settles it — it knows how each flag was declared, so the two readings are never told apart while the game runs. They compose rather than collide: slots[1][0] is the first character of what is in slot 1. A let and a proc's parameter go the same way as a plain flag, since neither can have slots:

proc initials(s) {
  let ws = split(s, " ")
  let k = 0
  let out = ""
  repeat length(ws) { let w = at(ws, k)  set out = out + upper(w[0])  set k = k + 1 }
  return out
}

On a value holding a list it reads one item, exactly as at() does — and it writes one, since a subscript past a flag's own shape reaches into the value it holds: see Deeper than a table. Text is the exception, because nothing in this language changes a piece of text in place: set q[1] = "X" where q holds "ali" reports (runtime error: q is text, not a list, so [1] cannot be written.) Build the new one with replace() or substr().

trim(s) takes blanks off both ends and leaves the middle alone. The blanks are a space, a tab, a newline and a carriage return — named here rather than left to each language's own idea of whitespace, which is how upper and lower came to be ASCII-only: JavaScript's trim() strips a page of Unicode spaces that C's isspace() does not, and a game would have tidied differently in a browser and a terminal.

set answer = trim(lower(reply))
set answer = replace(answer, "’", "'")       # a curly apostrophe is the same mark

if contains(lower(noun), "rope") { print "You mean the rope." }

Arithmetic

Numbers are ordinary numbers: 2.5 is one, 0.5 * 3 is 1.5, and a flag holds either a whole one or a fraction. A number may carry an exponent — 1e3, 1.5e-3, 2e+10 — which is the only way to write one too large to spell out. A number reads back the way it was written — the shortest run of digits that means exactly that number — and both runtimes agree on it down to the last digit, including 0.1 + 0.2, which is 0.30000000000000004 and not 0.3.

The engine's own lines follow the same rule. The HP in STATUS and in LOOK's list of enemies, the damage lines, EXAMINE, the score and a labelled stat are the engine's sentences rather than yours, and each prints a number exactly as print "hp=" + hp would. So a game with big numbers in it reads the same in a terminal and in a browser, and a stat of 99999999 says so rather than rounding itself up to a hundred million. Fractions are unaffected: 10.5 is 10.5, and a number past what a double can spell out keeps its exponent.

+ - * / % and the usual comparisons. / divides whole numbers as whole numbers — 7 / 2 is 3, not 3.5 — and keeps the fraction as soon as either side is written as a real: 7.0 / 2 and 7.5 / 2 are 3.5 and 3.75. That rule is its own section below. % is the remainder a truncating division leaves, so -7 % 2 is -1 and -7 / 2 * 2 + -7 % 2 is -7 again. Dividing or taking a modulus by zero gives zero rather than ending the story.

abs(x)      round(x)      floor(x)      ceil(x)      trunc(x)    sqrt(x)
min(a, b)   max(a, b)     pow(x, n)

trunc is how you ask for a whole number of times. It drops the fraction toward zero, where floor drops it downward: trunc(-7 / 2) is -3 and floor(-7 / 2) is -4. Counting things — a share each, a tenth of a stardate, a percentage that must be a whole number of chances — is trunc(a / b):

let each = trunc(amount / n)        # what each of them gets
let odds = trunc(base * dist / 12)  # a chance, which chance() wants whole

trunc(a / b) and a / b agree whenever a and b are both written as whole numbers, which is most of the time. trunc still earns its place on a real: trunc(7.5) is 7.

Whole numbers and reals

There is one number type and there are two kinds of number in it, and which kind a number is decides what / does with it.

3               a whole number
3.0             a real -- the same double, and not the same number
2.5             a real, and obviously one
1e3             a real: an exponent makes one, as a point does

A number written with a point or an exponent is a real, whatever it works out to. A number that carries a fraction is a real however it was written. Everything else is a whole number — and everything the engine hands a game is a whole number, because all of them are counts: length(), size(), hit points, a subscript, random(1, 6), the four rounding functions.

isint(3)        true
isint(3.0)      false         3.0 is a real that happens to have nothing after the point
isint(2.5)      false
3.0 is an int   false         the same question, read aloud

Two whole numbers divide as whole numbers. The fraction is dropped toward zero, the way trunc drops it:

3 / 2           1
10 / 4          2
-7 / 2          -3           toward zero, so `%` agrees with it

A real on either side keeps the fraction:

3.0 / 2         1.5
3 / 1.0         3            a real, and it divides like one
7.5 / 2         3.75
10 / 2.5        4            the answer is just whole; it is still a real

So counting things needs no trunc — amount / n is already the share each of them gets — and asking for the fraction is writing one of the two numbers as a real.

The rule reads what is written, not what arrived. 3.0 and 3 are the same double and divide differently, which is the whole point of having two kinds: a line of arithmetic does one thing, and it does it on every turn. The alternative — deciding from whether the values happened to come out whole — would make set share = total / people integer division on the turns when total divided exactly and fractional on the others, with nothing in the line to say which you were getting.

A value remembers which kind it is, through a flag, a slot, a list, a let and a proc's answer:

flag ratio = 1.0
flag count = 1

ratio / 2       0.5
count / 2       0

and arithmetic carries it: a real anywhere in a sum makes the answer one.

(2.0 + 1) / 2   1.5           2.0 + 1 is 3.0
(2 + 1) / 2     1             2 + 1 is 3
(1.5 * 2) / 2   1.5           1.5 * 2 is 3.0

Which kind each function answers with:

round, floor, ceil, truncalways a whole number — that is what they are for
abswhatever it was given
sqrta real: a square root is not a count
min, maxwhichever of the two it picked
powa real when its base is, since it is repeated multiplication
random(a, b), length, size, counta whole number
random()a real — it is a fraction

So round(v * 1000) / 1000 gives a whole number, because round hands back a whole one and 1000 is written as one. Rounding to three places is / 1000.0, and the point is doing the work:

let rounded = round(v * 1000) / 1000.0

trunc is still how you ask for a whole number of times on a value that has a fraction: trunc(7.5) is 7, and trunc(a / b) is whole division for two reals.

round rounds a half away from zero: round(2.5) is 3 and round(-2.5) is -3. Both runtimes work it out from floor rather than calling their own library's round, in the same three lines, and both are exact for every number.

That last word is load-bearing, and was not always true. The rounding used to be written as floor(x + 0.5), which rounds when it adds — so a value just below a half went up instead of down, and above about four and a half quadrillion, where consecutive doubles are a whole apart, x + 0.5 became x + 1 and round handed back a number one larger than the whole number it was given.

Calling the library looks like the fix and is not: one of the six toolchains this ships from has that same fault in its own round, so the language would have rounded differently on one platform. Working it out from floor avoids trusting six libraries to agree.

Anything cyclic is %: a day of the week is turn % 7, a dial that wraps is (dial + 1) % 12.

sqrt(x) and pow(x, y) are what a distance needs. Without them the only range a game could work out was the larger of the two gaps, which makes a diagonal the same length as a straight line half as long:

let dist = sqrt(pow(ax - bx, 2) + pow(ay - by, 2))

sqrt of a negative has no answer, so it says so and gives 0 — it is nearly always a subtraction that came out the wrong way round, which is the sort of mistake a game finds out about six turns later. pow of a negative base with a fractional exponent has no answer either, and says so the same way. Nought to a negative power is a division by nought, which is 0 here as everywhere.

pow takes any exponent, whole or fractional: pow(2, 10) is 1024, pow(2, 0.5) is sqrt(2) to the last bit, and pow(level, 1.5) is the curve you wanted.

Neither runtime asks its platform for it, and that is worth one paragraph because it is the only place in this language where the answer to an ordinary sum could have depended on where the game was played. C's pow() and JavaScript's Math.pow() disagree in the last bit for about a tenth of all fractional exponents — and "C's pow()" is not even one function, since the C library ships two builds of it and picks between them from the processor's feature bits. So both runtimes run one algorithm instead: a transcription of the C library's own, with its own constant tables, checked bit for bit against it over six hundred thousand pairs. The answer is the same in a terminal, in a browser, and on every machine.

The honest cost: the algorithm being copied is not perfectly rounded — it is out by one in the last place about once in a thousand — and copying it copies that. It is out by one in both runtimes, which is the property worth having, and it is four decimal places below anything a game prints. sqrt has no such trouble: IEEE 754 requires it to be correctly rounded, so every implementation agrees already.

These are keywords only where a ( follows, so a game that already has a flag called min or an item called round keeps both.

Statements

set flagName = expr
set hp = expr / set maxhp = expr   # the player's health; see "The player's own numbers"
play sceneId                       # narration; see "Scenes"
move npcId to roomId               # send somebody elsewhere; see "People"
move enemyId to roomId             # ...or something; it keeps its hit points
move itemId to roomId / to room / to inventory   # ...or a thing, which spawn would copy
provoke enemyId                    # that creature strikes the player, now; see "Enemies"
do procName                        # run a proc; see "Procedures"
do procName(expr, expr)            # ...passing arguments
set flagName[expr] = expr          # write one slot; see "Arrays"
set flagName[expr][expr] = expr    # ...or one of a table's, by row and column; see "Rows and columns"
set flagName[expr][expr][expr] = expr   # ...and past the shape, into the value; see "Deeper than a table"
shuffle flagName                   # ...or deal them all: 0..n-1 in a random order
sort flagName                      # ...or put its slots in order — see "Putting slots in order"
sort flagName by otherFlag         # ...or fill it with the drawing order that reads the other low to high
grow flagName                      # one more slot on a flag declared "[]" — see "A flag the game grows"
hide commandWord / show commandWord   # whether HELP offers it — see "Hiding a command from HELP"
hide (expr) / show (expr)          # ...naming the command while the game runs
print expr
if expr { ...statements... } else { ...statements... }   # see "Branching"
if expr { ... } else if expr { ... } else { ... }   # a chain; see "Branching"
repeat expr { ...statements... }   # run the block that many times; see "Repetition"
while expr { ...statements... }    # ...or for as long as the condition holds; see "Repetition"
break                              # leave the innermost loop; see "Leaving a loop early"
continue                           # ...or end this pass and start the next
let name = expr                    # a name for the rest of this block; see "Local values"
spawn itemId in room / in inventory / in roomId   # ...or a room named outright
spawn npcId in roomId / spawn enemyId in roomId   # people and monsters: rooms only
remove itemId from room / from inventory / from roomId
remove all itemId from room / from inventory / from roomId   # all of that one thing
remove all from room / from inventory / from roomId   # every item there
remove npcId from room             # from wherever they are
remove npcId from roomId           # ...or out of one named room
remove enemyId from room           # from the room the player is in
remove enemyId from roomId         # ...or out of one named room, wherever the player is
make enemyId attack                # the same as `provoke enemyId` — see "Readable spellings"
go to roomId                       # ...as `goto roomId`
goto questionId                    # ...a question is a CALL: it stops the block, and comes back — see "Questions"
the blow does not land             # ...as `refuse`, inside `on attack`
hurt expr / heal expr              # move the player's hit points — see "The player's own numbers"
pass / pass expr                   # spend a turn of the world's time, or several — see "Time"
reset turn                         # ...and the one way back: the count to zero, and `on turn` does not run — see "Time"
reset elapsed                      # ...the same for the other clock: the player's own seconds to zero — see "The clock on the wall"
set ehp(enemyId) = expr            # move a creature's — clamped, and 0 runs its `on defeat`
set maxehp(enemyId) = expr         # ...and the ceiling it is clamped to — see "The ceiling those hit points are held to"
refuse                             # inside `on attack`: this blow does not land; see "Who is striking: attacker"
equip itemId / unequip itemId      # put something in the player's hands, or take it out
return / return expr               # leave a proc, with an answer or without one — see "Procedures"
set itemId.damage = n              # a thing's own numbers — see "A thing's own numbers"
set creatureId.hp = n              # ...items have damage/defense/count/uses, creatures hp/maxhp/attack/defense
open exit <dir> -> roomId          # give this room a way out — see "Opening and closing a way out from a script"
close exit <dir>                   # ...or take one away
goto roomId                        # teleport the player; the new room is then shown,
                                   # exactly as though they had walked in
goto questionId / goto quiz[i]     # ...or into a question — see "Questions"
goto group(<expr>)[i]              # ...one of a group the game chose — see "A name built from a value"
board carId                        # put the player inside a vehicle standing here — see "Getting in and out"
board lifeboat[i] / board ferries[i]   # ...or one of a fleet — see "Several of a vehicle"
disembark                          # ...and take them out of it again
finish "The End."                  # ends the game

Expressions

true / false / numbers / "strings"   # see "Strings" for the escapes
flagName                           # reads a flag's current value
flagName[expr]                     # one of its slots; flagName[expr][expr] for a table — see "Rows and columns"
has(itemId)                        # is itemId in the player's inventory?
here(itemId)                       # is itemId in the current room?
playerroom                         # the room the player is in, as its id — see "Rooms"
playerregion                       # ...and the region that room belongs to, or "" — see "Regions"
playervehicle                      # ...and the vehicle they are aboard, or "" — see "Vehicles"
aboard(v)                          # is the player in that one? the word a room's exit wants — see "Driving, riding, sailing"
regionof(r)                        # which region any room belongs to, or "" — see "Regions"
roomof(x)                          # where an item, person, creature or vehicle is; "" for none — and stands where a room is named, see "What can stand where a room is named"
exitcount(r) / exitname(r, i)      # a room's ways out, counted and named — see "Rooms"
itemcount(r) / itemname(r, i)      # what is lying there — see "Reading what is in a room"
npccount(r) / npcname(r, i)        # ...who is standing there
enemycount(r) / enemyname(r, i)    # ...and what is standing there alive
exitto(r, dir) / exitopen(r, dir)   # where one leads, and whether it can be walked now — `exitto` stands where a room is named too
isopen(itemId)                     # is that openable item currently open?
carrying itemId                    # the same question as has() — see "Readable spellings"
noun means itemId                  # ...as noun_is(), and the same for a person or a creature
<dir> is passable from roomId      # ...as exitopen(); "barred" is the negation
there is a flag called <expr>      # ...as has_flag(); "there is no flag" is the negation
<expr> is a number                 # ...as isnumber(); "is not a number" is the negation
<expr> is an int                   # ...as isint(); "is not an int" is the negation
<text> contains <text>             # ...as contains()
<expr> in (<expr>, ...)            # equal to any of them: the same as the `==` chain — see "Readable spellings"
[<expr>, <expr>, ...]              # a list: several values under one name — see "Lists"
<expr> in <list>                   # is it one of them?
split(s, sep)                      # a string cut into a list; an empty separator cuts every character
join(list, sep)                    # ...and back into a string
at(list, i)                        # the i-th of it, counting from 0; 0 when there is no such one
<expr>[i]                          # one character of a string, or one item of a list — see "The string functions"
append(list, x)                    # a NEW list with x on the end
thingId is here                    # an item, a person OR a creature, in this room
x is alive / x is dead             # the same questions as alive() and not alive()
x is open / x is shut              # ...as isopen() and not isopen()
x is equipped                      # ...as equipped()
npcId is in roomId                 # ...as npcin()
member_here(series)                # which member of a creature or vehicle series is here, or -1
member                             # inside a series body: which member this is — see "A series of creatures"
equipped(itemId)                   # is that weapon or armour in hand right now?
npchere(npcId) / npcin(npcId, roomId)   # where somebody is standing
not expr / expr and expr / expr or expr
expr == expr, !=, <, >, <=, >=
expr + expr                        # string concatenation, or numeric addition
expr - expr  expr * expr           # numeric subtraction and multiplication
expr / expr                        # division, fraction and all (x/0 is 0)
expr % expr                        # the remainder a truncating division leaves — see "Arithmetic"
ehp(enemyId)                       # its hit points now — see "Enemies"
maxehp(enemyId)                    # ...and the ceiling they are held to — see "The ceiling those hit points are held to"
roomtitle(r)                       # the name a place is shown under, worked out now — see "Rooms"
count(itemId) / count_here(itemId)   # how many carried, how many here — see "Several of a thing"
size(seriesOrGroup)                # how many members it was declared with — see "Arrays"
size(group(<expr>))                # ...of a group named while the game runs, so not folded
flag(<expr>)[i]                    # one slot of an array named while the game runs
flag(<expr>)[i][j]                 # ...or one of a table's, by row and column
rows(flagName) / columns(flagName)   # the shape of a table — see "Rows and columns"
size(flagName)                     # ...and how many a flag declared "[]" has been grown to
has_flag(<expr>)                   # is there a flag by that name? — see "A name built from a value"
abs(x) round(x) floor(x) ceil(x) trunc(x) sqrt(x)   # see "Arithmetic"
min(a, b) / max(a, b) / pow(x, y)   # ...and pow's exponent may be fractional; see "Arithmetic"
length(s) upper(s) lower(s) trim(s)   # see "The string functions"
length(x)                          # ...and how many anything holds: a list, a group, a series, a flag's slots
contains(s, sub) / substr(s, at, n)
padright(s, n) / padleft(s, n)     # spaces to a width, so a table lines up — see "The string functions"
replace(s, from, to)               # every one of them, and never its own output
score / maxscore                   # the running total, if the game keeps one — see "Score"
random(n) / random(lo, hi)         # see "Randomness"
random()                           # ...and a fraction, 0 or more and less than 1 — see "Randomness"
chance(pct)
oneof(expr, expr, ...)
turn                               # how many turns have passed — see "Time"
elapsed                            # ...and how many seconds of the player's own — see "The clock on the wall"
carried / carried_kinds            # how much is in the pack — see "How much the player is carrying"
noun / other / prep                # inside a verb: the objects and the preposition
attacker                           # inside `on attack`: "player", or the creature's id — see "Who is striking: attacker"
reply                              # inside `on answer`: the line the player typed — see "Questions"
noun_is(itemId) / other_is(itemId)   # ...resolved against a real item
noun_is_npc(npcId) / other_is_npc(npcId)   # ...resolved against a person
noun_is_enemy(id) / other_is_enemy(id)   # ...resolved against a creature, the way ATTACK does
isnumber(<expr>)                   # is that text a number? — see "Reading a number the player typed"
isint(<expr>)                      # is that a whole number rather than a real? — see "Whole numbers and reals"
tonumber(<expr>)                   # ...that number, real if it is written with a point; 0 if it was not one
if expr then expr else expr        # a conditional expression — see "Conditions as values"

Precedence, from loosest to tightest

orleft-associative
andleft-associative
notprefix, binds tighter than and
== !=left-associative
< > <= >=left-associative
+ -left-associative
* / %left-associative
literals, flags, calls, ( … )

So 1 + 2 * 3 is 7, true or false and false is true, and not false and false is false — not takes only the thing immediately after it.

+ and - share a row, and that is worth one sentence of care. In a sentence built with +, a - that follows lands on the sentence rather than on the number:

print "had " + score - 1 + " right"      # ("had " + score) - 1 ... which is not a sentence

*, / and % bind tighter, so "" + 7 % 3 reads the way it looks; only - is caught by the tie. Put brackets round the sum, which is what you meant anyway:

print "had " + (score - 1) + " right"

The compiler refuses the first form wherever it can see that a side is text — a literal, a + chain built on one, or upper()/lower()/substr(). It cannot see inside a flag, so someFlag - 1 is still allowed: a flag may hold "5", and turning a typed number into a number is a rule worth keeping — see The string functions.

Relational and equality are separate rungs, so a comparison can feed an equality without brackets: 1 < 2 == true is (1 < 2) == true. Both are left-associative, so a < b < c means (a < b) < c — which compares a boolean with c and is almost certainly not what you meant. Bracket it if you find yourself wanting it.

Types, and what mixing them does

There are three: numbers, strings and booleans. Nothing is silently converted except by +.

"n=" + 3"n=3" — if either side is a string, + concatenates
"5" + 5"55" — likewise; there is no numeric parsing of strings
3 + 4 + "x""7x" — left-associative, so the numbers add first
1 == "1"false — == never converts; different types are never equal
7 / 2, -7 / 23, -3 — two whole numbers, truncated toward zero
7 / 2.0, 7.0 / 23.5 — a real on either side keeps the fraction
1 / 00 — division by zero is 0 rather than an error

-, * and / are numeric only.

When a string is used as a number

A string used where a number is wanted converts only if the whole of it is a number: optional surrounding blanks, an optional single + or -, digits, and optionally one decimal point with digits after it. Anything else is 0.

"42", "+7", "-0.5", " 12 "convert
"12abc", "abc", "12 x", ""0
"1e3", "0x10", "3."0

That is the same shape the language accepts for a number written in the source, with a sign allowed in front — a string converts exactly when it looks like a number you could have typed.

It applies to arithmetic and to comparison alike, so "12abc" - 1 is -1 and if "12abc" < 100 is true, both because the string counts as 0. Two strings still compare lexically: "abc" < "abd".

The rule is narrower than C's or JavaScript's own conversions, and deliberately so — those two disagree with each other, and this language is implemented in both.

What counts as true

if, and, or, not and every when clause ask the same question:

false, 0, ""false
true, any other number, any non-empty stringtrue

A flag declared flag n = 0 is therefore false until something raises it, which is usually what you want and is worth knowing before you rely on it.