Variables, scope, and memory

Scripts need to hold values while they run and remember facts between runs. Those are two different mechanisms: variables live only for the duration of one handler, while persistent state and tags survive from one event to the next. Mixing them up is a common early mistake, so this chapter draws the line clearly.

Local variables — let, set, const

let creates a fresh, mutable local; set rewrites an existing one; const creates one that can never be set. The name is written without $ when declaring; the $ appears only when reading:

after command (tally) {
  let n 0
  set n [+ $n 1]
  const LABEL "items"
  do "say $n $LABEL counted."
}

let shadows any outer binding of the same name; redeclaring a name already bound in the same scope is a compile-time error, and set of an unbound name (or a const) is too.

Local variables start fresh on every invocation and vanish when the handler ends — they do not carry from one event to the next. For that you need persistent state, below.

Scope: where names are visible

Names live in two separate namespaces that never collide. A bareword in command position (the first word of a statement, or inside [...]) resolves in the command namespace — built-ins and your defs. A $name resolves in the variable namespace — parameters, let/const locals, the handler-injected names ($self, $actor, …), and top-level consts. So let count 0 does not shadow the count built-in.

Blocks are lexically scoped: a name resolves up the chain of scopes where the block was written. A nested block sees the locals and injected names of the handler that encloses it:

after command (introduce) {
  let greeting "well met"
  each [/> $self room people] { <p>
    do "say $greeting, [name $p]."     # the block sees $greeting and $self
  }
}

The counter idiom

Because set searches outward through enclosing scopes, a block can update a variable owned by the scope around it. That is how you accumulate across a loop:

after command (count-players) {
  let total 0
  each [/> $self room creatures] { <c>
    if [isplayer $c] {
      set total [+ $total 1]    # mutates the outer 'total'
    }
  }
  do "say I count $total players."
}

The inner block reaches total through its enclosing scope, so each pass updates the same variable. (Structuring scripts shows the same trick used to build a reusable counter with def.)

Persistent state — store, recall, forget

To remember something between handler runs, write it to a game entity's persistent state: string-keyed slots attached to a creature, object, or room. This is not the variable namespace — you never read a slot with $name:

The entity is usually $self, the owner, so its memory lives as long as it does:

# remember a mood set on one event, react to it on another
after command (say) {
  require [keyword $args calm peace]
  store $self "mood" "serene"
  echo $actor "$self settles into a tranquil state."
}

after command (provoke) {
  if [eq [recall $self "mood"] "serene"] {
    do "say I was at peace... until now."
    forget $self "mood"
  }
}

A first-visit greeting is the canonical use — store a counter and check it:

after enter {
  require [isplayer $actor]
  let seen [recall $self "visits"]
  if [isnull $seen] {
    echo $actor "$self studies you, a stranger."
    store $self "visits" 1
  } else {
    echo $actor "$self nods — you have been here before."
  }
}

Slots are the script's own bookkeeping; they are not the entity's game attributes. A store $self "hp" 50 writes a slot named hp that has nothing to do with real hit points — those are read through built-ins like level and position.

Per-player flags — tags

For a simple yes/no fact attached to a player (not the script's owner), tags are lighter than slots. tag adds a marker, untag removes it, and hastag tests it. Tags live on players only, so guard with isplayer:

after command (say) {
  require [isplayer $actor]
  if [not [hastag $actor "greeted"]] {
    do "say Welcome! I will remember you."
    tag $actor "greeted"
  }
}

after command (forget-me) {
  require [isplayer $actor]
  untag $actor "greeted"
  do "say Very well — you are a stranger to me once more."
}

The greeting fires once; the greeted tag, carried by the player, suppresses it on later visits — even at a different mob that checks the same tag — until untag clears it. Note that if [not [hastag …]] { … } is the conditional form: the bare unless guard takes a condition and no body. Use a slot on $self to remember something about the owner; use a tag to remember something about the player.

See also