vhyl.t

Back to reference

   1  /* Adventure world policy: entities, actions, reports, actors and scheduling.
   2   * Copyright (c) 2026 John Cunningham. MIT License; see LICENSE. */
   3  
   4  /*
   5   * vhyl core: things, rooms, scope, and the command cycle.
   6   *
   7   * What a world declares is an entity and its rows. What this file supplies is
   8   * what every world would otherwise write again: what is in scope, what is
   9   * visible, how a room is described, and the verbs that move things about.
  10   */
  11  
  12  /** @api author
  13   * Base entity for physical and describable things in the adventure world. */
  14  class Thing: object
  15  
  16      /** @api author
  17       * Properties exposed to structured hosts for entity presentation. */
  18      presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
  19                      &isLightSource, &isFixed, &isDecoration, &isTransparent,
  20                      &isHidden, &bulk, &bulkCapacity, &objPos]
  21      /** @api author
  22       * Display name used by library messages and room descriptions. */
  23      name = nil
  24  /* A container holds things, and holds them shut when it can be closed. */
  25      /** @api author
  26       * Whether the thing can hold contained entities. */
  27      isContainer = nil
  28      /** @api author
  29       * Whether open and close actions may change this thing's state. */
  30      isOpenable = nil
  31      /** @api author
  32       * Current open state; defaults to open for non-openable things. */
  33      isOpen = true
  34  /* Fixed things stay where they are: scenery, fittings, rooms. */
  35      /** @api author
  36       * Prevent taking or moving this thing. */
  37      isFixed = nil
  38  /* Described in the room, or passed over as part of the furniture. */
  39      /** @api author
  40       * Mark scenery that is normally omitted from the room listing. */
  41      isDecoration = nil
  42  
  43      /** @api author
  44       * Description emitted when the thing is examined. */
  45      desc = nil
  46  /* Gives light when it is lit. A candle is a light source unlit. */
  47      /** @api author
  48       * Whether this thing can provide light when lit. */
  49      isLightSource = nil
  50      /** @api author
  51       * Current light state. */
  52      isLit = nil
  53  
  54      /** @api author
  55       * Return a candidate rank for this thing in the given action and role. */
  56      rank(a, role) { return isDecoration ? rankUnlikely : rankLogical; }
  57  
  58      cannotTakeMsg = nil
  59      cannotOpenMsg = nil
  60  
  61      /** @api author
  62       * Space this thing occupies in a container. */
  63      bulk = 1
  64      /** @api author
  65       * Total bulk this thing can hold when used as a container. */
  66      bulkCapacity = 0
  67  /* Whether what is inside is mentioned when the room is described. */
  68      /** @api author
  69       * Whether contents appear in a room description. */
  70      contentsListed = true
  71  /* The largest single thing that fits, whatever room is left. */
  72      /** @api author
  73       * Maximum bulk of one item accepted by this container. */
  74      maxSingleBulk = 0
  75  /* Seen into while shut. */
  76      /** @api author
  77       * Allow sight into this thing while closed. */
  78      isTransparent = nil
  79  
  80      isAttachable = nil
  81      /** @api author
  82       * Whether locking actions apply to this thing. */
  83      isLockable = nil
  84      /** @api author
  85       * Current locked state. */
  86      isLocked = nil
  87  /*
  88       * Being worn. On Thing rather than on Wearable for the same reason locking
  89       * is: a precondition has to be able to ask it of anything it is given.
  90       */
  91      /** @api author
  92       * Current worn state used by clothing actions and preconditions. */
  93      isWorn = nil
  94  /* What unlocks it. nil on a lockable thing means nothing does. */
  95      /** @api author
  96       * Item that unlocks this thing, or nil when no key does. */
  97      keyItem = nil
  98  
  99      isDistant = nil
 100  /* Whether subjects may be looked up in it; see Consultable. */
 101      isConsultable = nil
 102  /* Seen and never touched; see Intangible. */
 103      isIntangible = nil
 104  
 105      isOutOfReach = nil
 106      canObjReachSelf(who) { return nil; }
 107  
 108      specialDesc = nil
 109      hasSpecialDesc = nil
 110  
 111      allowedPostures = [standing]
 112      defaultPosture = standing
 113  
 114      roomBeforeAction(c) { return nil; }
 115  
 116      seen = nil
 117  
 118      isFamiliar = nil
 119  
 120      /** @api author
 121       * Test whether this knower knows an entity or fact tag. */
 122      knowsAbout(obj)
 123      {
 124          if (obj == nil)
 125              return nil;
 126  
 127          if (dataType(obj) == TypeSString)
 128              return holds(informedOf, obj);
 129          return obj.isFamiliar || knows.contains(self, obj);
 130      }
 131  
 132      /** @api author
 133       * Record knowledge of an entity or fact tag for this knower. */
 134      setKnowsAbout(obj)
 135      {
 136          if (obj == nil)
 137              return nil;
 138          if (dataType(obj) == TypeSString)
 139          {
 140              if (!holds(informedOf, obj))
 141                  informedOf = informedOf + [obj];
 142          }
 143          else if (!knows.contains(self, obj))
 144              knows.set(self, obj);
 145          return nil;
 146      }
 147  /* Forget a fact tag. Things are not forgotten; a fact can be superseded. */
 148      /** @api author
 149       * Remove a fact tag from this knower; entity knowledge remains. */
 150      forget(tag)
 151      {
 152          local kept = [];
 153          foreach (local t in informedOf)
 154          {
 155              if (t != tag)
 156                  kept = kept + [t];
 157          }
 158          informedOf = kept;
 159          return nil;
 160      }
 161  
 162      informedOf = []
 163  
 164      /** @api author
 165       * Subjects taught to this thing during vhylStart. */
 166      initiallyKnowsAbout = nil
 167  
 168      known = (player.knowsAbout(self))
 169  /*
 170       * A name that takes no article: a person, a place, a ship. vhyl had no such
 171       * notion, so a library message about an actor said *the Zoé*, and one about
 172       * the player said *the you*. What an article is doing there is the language
 173       * module's business; **whether the name takes one** is the world's.
 174       */
 175      /** @api author
 176       * Use a proper name without an article in English presentation. */
 177      isProper = nil
 178  
 179      isPlural = nil
 180      isMassNoun = nil
 181  
 182      isHim = nil
 183      isHer = nil
 184      isGenderNeutral = nil
 185  
 186      isIt = (!(isHim || isHer || isGenderNeutral))
 187  
 188      pcReferralPerson = SecondPerson
 189  
 190      bulkLimit = 0
 191  
 192      isEdible = nil
 193      isDrinkable = nil
 194      fluidName = nil
 195  /* Whether this will take something poured into it. */
 196      allowPourIntoMe = nil
 197  /*
 198       * Where a thing put here comes to rest. An enumerator, not a preposition:
 199       * the word for it belongs to the language module.
 200       */
 201      /** @api author
 202       * Position assigned to an item inserted here: in, on, under or behind. */
 203      objPos = posIn
 204  
 205      remapIn = nil
 206      remapOn = nil
 207      remapBehind = nil
 208  
 209      remapUnder = nil
 210  /* Out of sight until someone finds it. */
 211      /** @api author
 212       * Omit this thing from sight until revealed by an action. */
 213      isHidden = nil
 214  
 215      /** @api author
 216       * Observe an item being inserted after placement changes. */
 217      notifyInsert(item) { return nil; }
 218  
 219      /** @api author
 220       * Return nil to allow an action or text explaining a refusal. */
 221      refuses(action, c) { return nil; }
 222  
 223      /** @api author
 224       * Return preconditions for this object in the action and role. */
 225      preCond(a, role) { return []; }
 226  
 227      /** @api author
 228       * React before the traveller uses a connector. */
 229      beforeTravel(traveller, conn) { return nil; }
 230      /** @api author
 231       * React after a traveller uses a connector. */
 232      afterTravel(traveller, conn) { return nil; }
 233  ;
 234  
 235  /** @api author
 236   * A lit-by-default location whose exits and contents are relation rows. */
 237  class Room: Thing
 238      isFixed = true
 239  /* Lit by itself. A DarkRoom is not, and needs something carried. */
 240      isLit = true
 241  ;
 242  
 243  /* Somewhere the player cannot see without a light. */
 244  /** @api author
 245   * A room that requires another light source to be visible. */
 246  class DarkRoom: Room
 247      isLit = nil
 248  ;
 249  
 250  /* Outdoors: lit by the sky rather than by anything carried. */
 251  /** @api author
 252   * A room lit by the sky under the default world policy. */
 253  class OutdoorRoom: Room
 254      isLit = true
 255  ;
 256  
 257  /* Templates, so a declaration reads as a sentence. */
 258  Thing template'name';
 259  Room template'name';
 260  DarkRoom template'name';
 261  OutdoorRoom template'name';
 262  
 263  /*
 264   * The player. `vocab` so they can be referred to at all: without it `X ME`
 265   * answered *I don't understand that* in every game ever written against vhyl,
 266   * and looking at yourself is the first thing a good many players do.
 267   */
 268  /** @api author
 269   * The library player entity; place it by setting gameMain.initialRoom. */
 270  player: Thing'you'
 271      travelCycle = -1
 272      vocab ='me; my; myself self you'
 273  ;
 274  
 275  /** @api author
 276   * Per-game startup settings supplied by a modify gameMain declaration. */
 277  gameMain: object
 278      /** @api author
 279       * Room where the player is placed by vhylStart. */
 280      initialRoom = nil
 281  /* Displayed once at the start. An emitting string, like desc. */
 282      /** @api author
 283       * Text emitted once before the first room description. */
 284      intro =""
 285  
 286      /** @api author
 287       * Select past tense for English presentation. */
 288      usePastTense = nil
 289  ;
 290  
 291  /** @api host
 292   * Initialise the world once, place the player and describe the initial room. */
 293  vhylStart()
 294  {
 295  /* An emitting property displays when it is read; interpolating it would
 296       * print it and then interpolate the nil it answers. */
 297      gameMain.intro;
 298  "\n\n";
 299      if (gameMain.initialRoom == nil)
 300  "vhyl: no initial room; set `modify gameMain initialRoom = ...`.\n";
 301      else
 302      {
 303          contains.set(gameMain.initialRoom, player);
 304  
 305          for (local a = firstObj(Actor); a != nil; a = nextObj(a, Actor))
 306          {
 307              if (a.curState != nil)
 308                  continue;
 309              local first = nil;
 310              for (local st = firstObj(ActorState); st != nil; st = nextObj(st, ActorState))
 311              {
 312                  if (st.actor != a)
 313                      continue;
 314                  if (st.isInitState)
 315                  {
 316                      first = st;
 317                      break;
 318                  }
 319                  if (first == nil)
 320                      first = st;
 321              }
 322              if (first != nil)
 323                  a.setCurState(first);
 324          }
 325  
 326          for (local t = firstObj(Thing); t != nil; t = nextObj(t, Thing))
 327          {
 328              if (t.initiallyKnowsAbout == nil)
 329                  continue;
 330              foreach (local subject in t.initiallyKnowsAbout)
 331                  t.setKnowsAbout(subject);
 332          }
 333  
 334          for (local i = firstObj(InitObject); i != nil; i = nextObj(i, InitObject))
 335              i.execute();
 336  
 337          syncStates();
 338          describeRoom();
 339      }
 340      return nil;
 341  }
 342  
 343  /* ------------------------------------------------------------- directions */
 344  
 345  allDirections: object
 346      ways = [north, south, east, west,
 347              northeast, northwest, southeast, southwest,
 348              up, down, inward, outward]
 349  ;
 350  
 351  oppositeOf(way)
 352  {
 353      if (way == north) return south;
 354      if (way == south) return north;
 355      if (way == east) return west;
 356      if (way == west) return east;
 357      if (way == northeast) return southwest;
 358      if (way == southwest) return northeast;
 359      if (way == northwest) return southeast;
 360      if (way == southeast) return northwest;
 361      if (way == up) return down;
 362      if (way == down) return up;
 363      if (way == inward) return outward;
 364      if (way == outward) return inward;
 365      return nil;
 366  }
 367  
 368  /* Where this exit ends up, following a connector if there is one. */
 369  destinationOf(from, way)
 370  {
 371      local target = exits.get(from, way);
 372      if (target == nil || target.isHidden)
 373          return nil;
 374      if (!target.ofKind(TravelConnector))
 375          return target;
 376      if (target.ofKind(Door))
 377          return doorDestination(target, from);
 378      return target.destination;
 379  }
 380  
 381  wayFrom(from, to)
 382  {
 383      if (from == nil || to == nil)
 384          return nil;
 385      foreach (local way in allDirections.ways)
 386      {
 387          if (destinationOf(from, way) == to)
 388              return way;
 389      }
 390      return nil;
 391  }
 392  
 393  /* ---------------------------------------------------------------- queries */
 394  
 395  /** @api author
 396   * Return the room containing the player. */
 397  here()
 398  {
 399      return contains.outermost(player);
 400  }
 401  
 402  litBy(room)
 403  {
 404      foreach (local item in contains.all(room))
 405      {
 406          if (item.isLightSource && item.isLit)
 407              return true;
 408  /*
 409           * Light gets out of anything that is not shut: an open box, a pocket,
 410           * and the player, who is not a container but is carrying the torch.
 411           * Containment is one table, so the walk does not care which it is.
 412           */
 413          if (item.isContainer && item.isOpenable && !item.isOpen)
 414              continue;
 415          if (litBy(item))
 416              return true;
 417      }
 418      return nil;
 419  }
 420  
 421  /* A room you can see in: lit itself, or by something in it or carried. */
 422  isDark()
 423  {
 424      local room = here();
 425      if (room.isLit)
 426          return nil;
 427      return !litBy(room);
 428  }
 429  
 430  /*
 431   * Visible means reachable without opening anything: every container between the
 432   * item and the room has to be open. One walk up the containment table.
 433   */
 434  visible(item)
 435  {
 436      local room = here();
 437      foreach (local step in contains.ancestors(item))
 438      {
 439          if (step == room || step == player)
 440              return true;
 441  
 442          if (step.isContainer && step.isOpenable && !step.isOpen
 443              && !step.isTransparent)
 444              return nil;
 445      }
 446      return nil;
 447  }
 448  
 449  inScope(item)
 450  {
 451  /*
 452       * The player is in scope, and used not to be — which meant `X ME` answered
 453       * *I don't see that* in every game ever written against vhyl, and looking
 454       * at yourself is among the first things a good many players do.
 455       *
 456       * Being in scope is not the same as being a likely candidate: the player
 457       * ranks itself down for everything but examining (below), so a word that
 458       * names both you and something else still means the something else.
 459       */
 460      if (item == player)
 461          return true;
 462  
 463      if (item.isHidden)
 464          return nil;
 465  /* In the dark you can still reach what you are carrying, and nothing else. */
 466      if (isDark())
 467          return carried(item);
 468  /*
 469       * A nested space may confine you. Sitting in a booth whose reachOut is nil,
 470       * you can touch what is in the booth and what you are carrying and nothing
 471       * else — which is what nested rooms are for.
 472       */
 473      local nest = nestedIn();
 474      if (nest != nil && !nest.reachOut && item != nest)
 475          return carried(item) || holds(contains.all(nest), item);
 476  /* A door stands in both the rooms it joins, so it is in scope from either. */
 477      if (item.ofKind(TravelConnector))
 478          return connectorHere(item) || visible(item);
 479  /* A MultiLoc is in scope in any room it is present in. */
 480      if (item.isMultiLoc)
 481          return presentIn.contains(item, here());
 482  
 483      if (item.isDistant)
 484          return nil;
 485      return visible(item);
 486  }
 487  
 488  carried(item)
 489  {
 490      return location.get(item) == player;
 491  }
 492  
 493  /* ------------------------------------------------------------ description */
 494  
 495  /*
 496   * Whether what is inside can be seen. Open, or not shuttable, or shut but
 497   * transparent — a glass jar with the lid on shows what is in it.
 498   */
 499  seeInto(item)
 500  {
 501      if (!item.isOpenable)
 502          return true;
 503      return item.isOpen || item.isTransparent;
 504  }
 505  
 506  listContents(room)
 507  {
 508      foreach (local item in contains.all(room))
 509      {
 510  /*
 511           * Fixtures belong to the room's own description, not to the list of
 512           * what is lying about in it. A desk is part of the study; a trolley is
 513           * something someone left there.
 514           */
 515  
 516          if (item.isHidden)
 517              continue;
 518  
 519          item.seen = true;
 520          player.setKnowsAbout(item);
 521          if (item == player || item.isDecoration || item.isFixed)
 522              continue;
 523          sayFor('room.thereis', item, nil);
 524          if (item.isContainer && item.isOpenable && !item.isOpen && !item.isTransparent)
 525              sayFor('room.andclosed', item, nil);
 526          else if (item.isContainer && item.contentsListed && seeInto(item))
 527          {
 528              local inside = [for held in contains.all(item) : held if !held.isHidden];
 529              if (inside.length() > 0)
 530              {
 531                  noteSeen(inside);
 532                  sayAbout('room.andholding', item, inside, nil);
 533              }
 534          }
 535          sayMsg('room.endofitem', nil);
 536      }
 537  
 538      foreach (local item in roomSpecials(room))
 539      {
 540          local state = item.isActor ? item.curState : nil;
 541          if (state != nil && state.hasSpecialDesc)
 542          {
 543  "\n";
 544              state.specialDesc;
 545          }
 546          else if (item.hasSpecialDesc)
 547          {
 548  "\n";
 549              item.specialDesc;
 550          }
 551          else if (item.isActor)
 552          {
 553  "\n";
 554              sayFor('room.actorhere', item, nil);
 555          }
 556      }
 557      return nil;
 558  }
 559  
 560  roomSpecials(room)
 561  {
 562      local found = [];
 563      foreach (local item in contains.all(room))
 564      {
 565          if (item.isHidden || item == player)
 566              continue;
 567          if (item.isActor || item.hasSpecialDesc)
 568              found = found + [item];
 569  /*
 570           * A glass booth, an open crate, a chain hanging off a gatepost: what
 571           * is inside is plainly there. `seeInto` is the whole test — a thing
 572           * that does not shut never hides what it holds — and descending only
 573           * matters for something that asked for a line, so nothing is listed
 574           * that did not opt in.
 575           */
 576          if (!seeInto(item))
 577              continue;
 578          foreach (local inside in contains.all(item))
 579          {
 580              if (inside.isHidden || inside == player)
 581                  continue;
 582              if (inside.isActor || inside.hasSpecialDesc)
 583                  found = found + [inside];
 584          }
 585      }
 586      return found;
 587  }
 588  
 589  describeRoom()
 590  {
 591      local room = here();
 592  
 593      sayFor('room.enter', room, nil);
 594      if (isDark())
 595      {
 596          sayMsg('room.dark', nil);
 597  "\n\n";
 598          return nil;
 599      }
 600  
 601      sayFor('room.name', room, nil);
 602  "\n";
 603      room.seen = true;
 604      player.setKnowsAbout(room);
 605      room.desc;
 606  "\n";
 607      listContents(room);
 608      listSensed(room);
 609  "\n";
 610      return nil;
 611  }
 612  
 613  /* --------------------------------------------------------------- messages */
 614  
 615  /** @api author
 616   * A message keyed by stable id, with text or a computed say method. */
 617  class Message: object
 618      /** @api author
 619       * Stable key used by actions and host presentation. */
 620      id = nil
 621  
 622      /** @api author
 623       * Literal presentation text for this message. */
 624      text = nil
 625      /** @api author
 626       * Emit a computed message for the supplied command. */
 627      say(c) { return nil; }
 628  /* Higher wins. A game's overrides sit above vhyl's, which are all 0. */
 629      /** @api author
 630       * Higher-priority active messages replace lower-priority messages with the same id. */
 631      priority = 0
 632  /* Answered nil to stand aside: a message set that only applies sometimes. */
 633      /** @api author
 634       * Observational condition controlling whether this message can be selected. */
 635      isActive = true
 636  ;
 637  
 638  msgArgs: Command;
 639  
 640  /* Say an id about one or two objects, with no command in hand. */
 641  sayFor(id, a, b)
 642  {
 643      msgArgs.dobj = a;
 644      msgArgs.iobj = b;
 645      msgArgs.list_ = nil;
 646      msgArgs.way_ = nil;
 647      return sayMsg(id, msgArgs);
 648  }
 649  
 650  account(id, c)
 651  {
 652      return accountAbout(id, c, nil);
 653  }
 654  
 655  accountAbout(id, c, word)
 656  {
 657      return accountAs(2, id, c, word);
 658  }
 659  
 660  accountDefault(id, c)
 661  {
 662      return accountAs(4, id, c, nil);
 663  }
 664  
 665  /* One account, at a given place in the report order. */
 666  accountAs(where, id, c, word)
 667  {
 668      local done = c.done_;
 669      if (done == nil || done.length() == 0)
 670          return nil;
 671  
 672      if (done.length() > 1 && messageFor(id +'.several') != nil)
 673          id = id +'.several';
 674      local r = addReport(c, where, id, done[1], c.iobj);
 675      if (r != nil)
 676      {
 677          r.list = done;
 678          r.word = word;
 679      }
 680      return nil;
 681  }
 682  
 683  noteDone(c, item)
 684  {
 685      c.done_ = c.done_ == nil ? [item] : c.done_ + [item];
 686      return nil;
 687  }
 688  
 689  /*
 690   * The text of a message about one or two objects, **read** rather than said.
 691   *
 692   * `refuses` answers a sentence for the turn to print, so it needs the words
 693   * rather than the side effect. A message with no text — one whose body emits —
 694   * has nothing to read, and answers nil so the caller falls back to whatever it
 695   * would have said anyway.
 696   */
 697  msgFor(id, a, b)
 698  {
 699      local m = messageFor(id);
 700      if (m == nil || m.text == nil)
 701          return nil;
 702      msgArgs.dobj = a;
 703      msgArgs.iobj = b;
 704      msgArgs.list_ = nil;
 705      msgArgs.way_ = nil;
 706      return language.expand(m.text, msgArgs);
 707  }
 708  
 709  /* Say an id about an object and a list of things, or a word. */
 710  sayAbout(id, a, list, word)
 711  {
 712      msgArgs.dobj = a;
 713      msgArgs.iobj = nil;
 714      msgArgs.list_ = list;
 715      msgArgs.way_ = word;
 716      return sayMsg(id, msgArgs);
 717  }
 718  
 719  /* The message that answers to this id, or nil. */
 720  messageFor(id)
 721  {
 722      local best = nil;
 723      for (local m = firstObj(Message); m != nil; m = nextObj(m, Message))
 724      {
 725          if (m.id != id || !m.isActive)
 726              continue;
 727          if (best == nil || m.priority > best.priority)
 728              best = m;
 729      }
 730      return best;
 731  }
 732  
 733  /*
 734   * Say what this id means. `c` is the command, so a message that wants the
 735   * objects has them; one that does not ignores it.
 736   *
 737   * An unknown id prints the id itself rather than nothing, because a message
 738   * that silently says nothing is the hardest kind to find.
 739   */
 740  sayMsg(id, c)
 741  {
 742  
 743      event(id);
 744      if (c != nil)
 745      {
 746  
 747          local named = [];
 748          if (c.dobj != nil)
 749              named = named + [c.dobj];
 750          if (c.iobj != nil && !holds(named, c.iobj))
 751              named = named + [c.iobj];
 752          if (c.list_ != nil)
 753          {
 754              foreach (local item in c.list_)
 755              {
 756                  if (!holds(named, item))
 757                      named = named + [item];
 758              }
 759          }
 760          foreach (local item in named)
 761              eventSubject(item);
 762      }
 763      local m = messageFor(id);
 764      if (m == nil)
 765      {
 766  "[no message: <<id>>]\n";
 767          return nil;
 768      }
 769      if (m.text != nil)
 770  "<<language.expand(m.text, c)>>";
 771      else
 772          m.say(c);
 773      return nil;
 774  }
 775  
 776  language: object
 777      expand(text, c) { return text; }
 778  ;
 779  
 780  /*
 781   * The player is the last thing a command means when it could mean anything
 782   * else, and is not something to be picked up. `isFixed` is what refuses TAKE ME
 783   * with a sentence rather than with a silence.
 784   */
 785  modify player
 786      isFixed = true
 787      rank(a, role) { return a == examineAction ? rankLogical : rankSelf; }
 788  ;
 789  
 790  /* ------------------------------------------------------------ verb codes */
 791  
 792  /** @api host
 793   * Maps a stable structured action code to an Action and optional direction. */
 794  class Verb: object
 795  /* What the host sends. Stable; a code is never reused for something else. */
 796      /** @api host
 797       * Stable numeric code submitted by a structured-action host. */
 798      code = 0
 799  /* The action it runs. */
 800      /** @api author
 801       * Action executed for this verb code. */
 802      action = nil
 803  /* A direction, for the travel verbs. */
 804      /** @api author
 805       * Direction supplied to the action for travel verb codes. */
 806      dir = nil
 807  /* For the export. Not shown to a player: it is a key, not a word. */
 808      /** @api host
 809       * Stable key exported for host use; it is not player-visible prose. */
 810      label = nil
 811  
 812      verbPhrase = nil
 813  
 814      preposition()
 815      {
 816          local phrase = verbPhrase;
 817          if (phrase == nil)
 818              return nil;
 819          local open = phrase.find('(');
 820          if (open == nil)
 821              return nil;
 822          local rest = phrase.substr(open + 1);
 823  /* The **first** group only, and not past its closing bracket: an
 824           * `unlock/unlocking (what) (with what)` has a preposition in its
 825           * second group and none in its first, and reading past the bracket
 826           * printed *(first unlocking what) the locker)*. */
 827          local close = rest.find(')');
 828          if (close != nil)
 829              rest = rest.substr(1, close - 1);
 830          local space = rest.find(' ');
 831          if (space == nil)
 832              return nil;
 833          local word = rest.substr(1, space - 1);
 834          return word is in ('what','whom') ? nil : word;
 835      }
 836  /* The gerund alone: 'taking' from 'take/taking (what)'. */
 837      gerund()
 838      {
 839          local phrase = verbPhrase;
 840          if (phrase == nil)
 841              return label == nil ?'' : label +'ing';
 842          local slash = phrase.find('/');
 843          if (slash == nil)
 844              return phrase;
 845          local rest = phrase.substr(slash + 1);
 846          local space = rest.find(' ');
 847          return space == nil ? rest : rest.substr(1, space - 1);
 848      }
 849  ;
 850  
 851  /* The verb this code names, or nil. */
 852  verbFor(code)
 853  {
 854      for (local v = firstObj(Verb); v != nil; v = nextObj(v, Verb))
 855      {
 856          if (v.code == code)
 857              return v;
 858      }
 859      return nil;
 860  }
 861  
 862  /* Nothing to say (1-9). */
 863  vLook:       Verb code = 1  action = lookAction      label ='look' verbPhrase ='look/looking around';
 864  vInventory:  Verb code = 2  action = inventoryAction label ='inventory' verbPhrase ='take/taking inventory';
 865  vUndo:       Verb code = 3  action = undoAction      label ='undo' verbPhrase ='undo/undoing';
 866  vScore:      Verb code = 4  action = scoreAction     label ='score' verbPhrase ='check/checking (the score)';
 867  vGetOff:     Verb code = 5  action = getOffAction    label ='get-off' verbPhrase ='get/getting off';
 868  vWait:       Verb code = 6  action = waitAction      label ='wait' verbPhrase ='wait/waiting';
 869  vStand:      Verb code = 7  action = standAction     label ='stand' verbPhrase ='stand/standing up';
 870  vSit:        Verb code = 8  action = sitAction       label ='sit' verbPhrase ='sit/sitting down';
 871  vLie:        Verb code = 9  action = lieAction       label ='lie' verbPhrase ='lie/lying down';
 872  vStandOn:    Verb code = 43 action = standOnAction   label ='stand-on' verbPhrase ='stand/standing (on what)';
 873  vSitOn:      Verb code = 44 action = sitOnAction     label ='sit-on' verbPhrase ='sit/sitting (on what)';
 874  vLieOn:      Verb code = 45 action = lieOnAction     label ='lie-on' verbPhrase ='lie/lying (on what)';
 875  vSay:        Verb code = 46 action = sayAction        label ='say' verbPhrase ='say/saying (what)';
 876  vWriteOn:    Verb code = 47 action = writeOnAction    label ='write-on' verbPhrase ='write/writing (what) (on what)';
 877  vThinkAbout: Verb code = 48 action = thinkAboutAction label ='think-about' verbPhrase ='think/thinking (about what)';
 878  vConsult:    Verb code = 49 action = consultAction    label ='consult' verbPhrase ='look/looking up (what) (in what)';
 879  vGive:       Verb code = 58 action = giveAction      label ='give-to' verbPhrase ='give/giving (what) (to whom)';
 880  vShow:       Verb code = 59 action = showAction      label ='show-to' verbPhrase ='show/showing (what) (to whom)';
 881  vUnlock:     Verb code = 41 action = unlockAction    label ='unlock' verbPhrase ='unlock/unlocking (what) (with what)';
 882  vLock:       Verb code = 42 action = lockAction      label ='lock' verbPhrase ='lock/locking (what) (with what)';
 883  
 884  vNorth:      Verb code = 10 action = goAction dir = north     label ='go-north' verbPhrase ='go/going north';
 885  vSouth:      Verb code = 11 action = goAction dir = south     label ='go-south' verbPhrase ='go/going south';
 886  vEast:       Verb code = 12 action = goAction dir = east      label ='go-east' verbPhrase ='go/going east';
 887  vWest:       Verb code = 13 action = goAction dir = west      label ='go-west' verbPhrase ='go/going west';
 888  vUp:         Verb code = 14 action = goAction dir = up        label ='go-up' verbPhrase ='go/going up';
 889  vDown:       Verb code = 15 action = goAction dir = down      label ='go-down' verbPhrase ='go/going down';
 890  vIn:         Verb code = 16 action = goAction dir = inward    label ='go-in' verbPhrase ='go/going in';
 891  vOut:        Verb code = 17 action = goAction dir = outward   label ='go-out' verbPhrase ='go/going out';
 892  
 893  /* One subject (20-49). */
 894  vExamine:    Verb code = 20 action = examineAction    label ='examine' verbPhrase ='examine/examining (what)';
 895  vTake:       Verb code = 21 action = takeAction       label ='take' verbPhrase ='take/taking (what)';
 896  vDrop:       Verb code = 22 action = dropAction       label ='drop' verbPhrase ='drop/dropping (what)';
 897  vOpen:       Verb code = 23 action = openAction       label ='open' verbPhrase ='open/opening (what)';
 898  vClose:      Verb code = 24 action = closeAction      label ='close' verbPhrase ='close/closing (what)';
 899  vTurn:       Verb code = 25 action = turnAction       label ='turn' verbPhrase ='turn/turning (what)';
 900  vTurnOn:     Verb code = 26 action = turnOnAction     label ='turn-on' verbPhrase ='turn on (what)';
 901  vTurnOff:    Verb code = 27 action = turnOffAction    label ='turn-off' verbPhrase ='turn off (what)';
 902  vFlip:       Verb code = 28 action = flipAction       label ='flip' verbPhrase ='flip/flipping (what)';
 903  vPress:      Verb code = 29 action = pressAction      label ='press' verbPhrase ='press/pressing (what)';
 904  vBoard:      Verb code = 30 action = boardAction      label ='board' verbPhrase ='board/boarding (what)';
 905  vEnter:      Verb code = 31 action = enterAction      label ='enter' verbPhrase ='enter/entering (what)';
 906  vWear:       Verb code = 32 action = wearAction       label ='wear' verbPhrase ='wear/wearing (what)';
 907  vDoff:       Verb code = 33 action = doffAction       label ='doff' verbPhrase ='take off (what)';
 908  vLookUnder:  Verb code = 34 action = lookUnderAction  label ='look-under' verbPhrase ='look/looking (under what)';
 909  vLookBehind: Verb code = 35 action = lookBehindAction label ='look-behind' verbPhrase ='look/looking (behind what)';
 910  vListen:     Verb code = 36 action = listenAction     label ='listen' verbPhrase ='listen/listening (to what)';
 911  vSmell:      Verb code = 37 action = smellAction      label ='smell' verbPhrase ='smell/smelling (what)';
 912  
 913  /* Two subjects (50-59). */
 914  vPutOn:      Verb code = 50 action = putOnAction      label ='put-on' verbPhrase ='put/putting (what) (on what)';
 915  vPutIn:      Verb code = 51 action = putInAction      label ='put-in' verbPhrase ='put/putting (what) (in what)';
 916  vPutBehind:  Verb code = 52 action = putBehindAction  label ='put-behind' verbPhrase ='put/putting (what) (behind what)';
 917  vAttach:     Verb code = 53 action = attachAction     label ='attach' verbPhrase ='attach/attaching (what) (to what)';
 918  vDetach:     Verb code = 54 action = detachAction     label ='detach' verbPhrase ='detach/detaching (what) (from what)';
 919  vAskAbout:   Verb code = 55 action = askAboutAction   label ='ask-about' verbPhrase ='ask/asking (whom) (about what)';
 920  vTellAbout:  Verb code = 56 action = tellAboutAction  label ='tell-about' verbPhrase ='tell/telling (whom) (about what)';
 921  
 922  /* A subject and a direction (60-69). */
 923  vEat:        Verb code = 38 action = eatAction        label ='eat' verbPhrase ='eat/eating (what)';
 924  vDrink:      Verb code = 39 action = drinkAction      label ='drink' verbPhrase ='drink/drinking (what)';
 925  vDetachPart: Verb code = 40 action = detachPartAction label ='detach-part' verbPhrase ='detach/detaching (what)';
 926  vPourInto:   Verb code = 57 action = pourIntoAction   label ='pour-into' verbPhrase ='pour/pouring (what) (into what)';
 927  
 928  vPushNorth:  Verb code = 60 action = pushDirAction dir = north label ='push-north' verbPhrase ='push/pushing (what) north';
 929  vPushSouth:  Verb code = 61 action = pushDirAction dir = south label ='push-south' verbPhrase ='push/pushing (what) south';
 930  vPushEast:   Verb code = 62 action = pushDirAction dir = east  label ='push-east' verbPhrase ='push/pushing (what) east';
 931  vPushWest:   Verb code = 63 action = pushDirAction dir = west  label ='push-west' verbPhrase ='push/pushing (what) west';
 932  
 933  /* ---------------------------------------------------------------- actions */
 934  
 935  /** @api author
 936   * Defines argument requirements and the execution and reporting hooks for a command. */
 937  class Action: object
 938  /* Whether the turn resolves a direct and an indirect object first. */
 939      /** @api author
 940       * Whether the command requires a direct entity argument. */
 941      needsDobj = nil
 942      /** @api author
 943       * Whether the command requires an indirect entity argument. */
 944      needsIobj = nil
 945  
 946      /** @api author
 947       * Treat the indirect argument as a topic instead of an ordinary thing. */
 948      iobjIsTopic = nil
 949  
 950      /** @api author
 951       * Treat the direct argument as a topic instead of an ordinary thing. */
 952      dobjIsTopic = nil
 953  
 954      /** @api author
 955       * Message id used when the required direct argument is absent. */
 956      missingDobjMsg ='parse.noref'
 957      /** @api author
 958       * Message id used when the required indirect argument is absent. */
 959      missingIobjMsg ='parse.noref'
 960  /*
 961       * What the action does. `c` is the command, carrying the resolved dobj and
 962       * iobj and whatever else the rule captured, so an action that wants a
 963       * direction or a word reads it there.
 964       */
 965      /** @api author
 966       * Perform the action after resolution and preconditions; c holds resolved objects. */
 967      exec(c) { sayMsg('action.nothing', c);"\n"; return nil; }
 968  
 969      /** @api author
 970       * Return an action-specific candidate rank for an argument and role. */
 971      rank(item, role) { return rankLogical; }
 972  
 973      /** @api author
 974       * Require the selected object to be reachable for this action. */
 975      needsReach = true
 976  
 977      /** @api author
 978       * Return the preconditions needed for an argument in its resolved role. */
 979      preCond(item, role) { return []; }
 980  
 981      /** @api author
 982       * Produce the action's report after successful execution. */
 983      report(c) { return nil; }
 984  ;
 985  
 986  /* The command a rule produces, and what the turn resolves into it. */
 987  /** @api author
 988   * Carries parser captures, resolved arguments and reports through an action. */
 989  class Command: object
 990      action = nil
 991  
 992      set_ = nil
 993  
 994      dobj_ = nil
 995      iobj_ = nil
 996  /* What the turn resolved them to. */
 997      /** @api author
 998       * Resolved direct object, available in action hooks. */
 999      dobj = nil
1000      /** @api author
1001       * Resolved indirect object, available in action hooks. */
1002      iobj = nil
1003  
1004      way_ = nil
1005  
1006      dir_ = nil
1007  /* Things a message has to name several of. */
1008      list_ = nil
1009  
1010      done_ = nil
1011  /* What the command still has to say, in the order it will say it. */
1012      reports_ = nil
1013  
1014      failed_ = nil
1015  
1016      implied_ = nil
1017  
1018      actorPhrase_ = nil
1019      inner_ = nil
1020  /* A literal phrase the rule captured, as words rather than a name. */
1021      literal_ = nil
1022  
1023      topicText_ = nil
1024  ;
1025  
1026  /* ------------------------------------------------------------- reporting */
1027  
1028  class Report: object
1029  
1030      place = 2
1031      id = nil
1032      dobj = nil
1033      iobj = nil
1034      list = nil
1035  
1036      reveals = nil
1037      word = nil
1038      construct(where, what, a, b) { place = where; id = what; dobj = a; iobj = b; }
1039  ;
1040  
1041  /* A displayed discovery is world knowledge, including for a host using only
1042   * semantic events. Do not infer this from arbitrary message subjects. */
1043  noteSeen(items)
1044  {
1045      foreach (local item in items)
1046      {
1047          item.seen = true;
1048          player.setKnowsAbout(item);
1049      }
1050      return nil;
1051  }
1052  
1053  /* Put a report on the command's list. */
1054  addReport(c, where, id, a, b)
1055  {
1056      if (c == nil || id == nil)
1057          return nil;
1058      local r = new Report(where, id, a, b);
1059      c.reports_ = c.reports_ == nil ? [r] : c.reports_ + [r];
1060      return r;
1061  }
1062  
1063  /* The main result of the action. There may be several. */
1064  mainReport(c, id, a, b) { return addReport(c, 2, id, a, b); }
1065  
1066  reportBefore(c, id, a, b) { return addReport(c, 1, id, a, b); }
1067  /* Moved after every main report, however early it is added. */
1068  reportAfter(c, id, a, b) { return addReport(c, 3, id, a, b); }
1069  /*
1070   * A main report that also marks the command a failure, so a caller that ran it
1071   * as an implied action knows not to go on.
1072   */
1073  reportFailure(c, id, a, b)
1074  {
1075      if (c != nil)
1076          c.failed_ = true;
1077      return addReport(c, 2, id, a, b);
1078  }
1079  
1080  defaultReport(c, id, a, b) { return addReport(c, 4, id, a, b); }
1081  /*
1082   * The same, for a description: *You see nothing unusual.* Dropped when
1083   * anything else is said, but **kept** for an implied command, because a
1084   * description is an answer rather than an acknowledgement.
1085   */
1086  defaultDescReport(c, id, a, b) { return addReport(c, 5, id, a, b); }
1087  /* An extra line that does not suppress a default. */
1088  extraReport(c, id, a, b) { return addReport(c, 6, id, a, b); }
1089  
1090  sayReports(c)
1091  {
1092      local list = c.reports_;
1093      c.reports_ = nil;
1094      if (list == nil || list.length() == 0)
1095          return nil;
1096      local real = nil;
1097      foreach (local r in list)
1098      {
1099          if (r.place <= 3)
1100              real = true;
1101      }
1102      foreach (local where in [1, 2, 4, 5, 3, 6])
1103      {
1104          if (real && (where == 4 || where == 5))
1105              continue;
1106          if (c.implied_ && where == 4)
1107              continue;
1108          foreach (local r in list)
1109          {
1110              if (r.place != where)
1111                  continue;
1112              msgArgs.dobj = r.dobj;
1113              msgArgs.iobj = r.iobj;
1114              msgArgs.list_ = r.list;
1115              msgArgs.way_ = r.word;
1116              if (r.reveals != nil)
1117                  noteSeen(r.reveals);
1118              sayMsg(r.id, msgArgs);
1119  "\n";
1120          }
1121      }
1122      return nil;
1123  }
1124  
1125  lookAction: Action
1126      exec(c) { return describeRoom(); }
1127  ;
1128  
1129  inventoryAction: Action
1130      exec(c) { return doInventory(); }
1131  ;
1132  
1133  undoAction: Action
1134      exec(c)
1135      {
1136          if (undo())
1137              sayMsg('undo.ok', c);
1138          else
1139              sayMsg('undo.none', c);
1140  "\n";
1141          return nil;
1142      }
1143  ;
1144  
1145  goAction: Action
1146      exec(c) { return doGo(c.dir_); }
1147  ;
1148  
1149  getOffAction: Action
1150      exec(c) { return doGetOff(); }
1151  ;
1152  
1153  /*
1154   * WAIT. Nothing happens except a turn passing, which is the point: a game with
1155   * daemons, fuses and actors needs a way to let them act. vhyl had no such verb
1156   * at all, so every program that wanted one declared it — which is the sort of
1157   * thing a library exists to stop.
1158   */
1159  /* ------------------------------------------------------------- postures */
1160  
1161  takePosture(c, item, want)
1162  {
1163      local where = item == nil ? nestedIn() : item;
1164  /*
1165       * STAND with nothing named, while already standing: say so. Without this
1166       * a player standing on the table who typed STAND climbed the table again.
1167       */
1168      if (item == nil && player.posture == want)
1169      {
1170          sayMsg('posture.already', c);
1171  "\n";
1172          return nil;
1173      }
1174  /* STAND with nothing named, and nothing to stand on: get up. */
1175      if (where == nil)
1176      {
1177          if (want == standing && player.posture == standing)
1178          {
1179              sayMsg('posture.already', c);
1180  "\n";
1181              return nil;
1182          }
1183          if (want != standing)
1184          {
1185              sayMsg('posture.nowhere', c);
1186  "\n";
1187              return nil;
1188          }
1189          player.posture = standing;
1190          noteDone(c, player);
1191          return nil;
1192      }
1193      if (!inScope(where))
1194      {
1195          sayFor('posture.cannot', where, nil);
1196  "\n";
1197          return nil;
1198      }
1199      if (!where.isEnterable && !where.isVehicle)
1200      {
1201          sayFor('posture.cannot', where, nil);
1202  "\n";
1203          return nil;
1204      }
1205      if (!holds(where.allowedPostures, want))
1206      {
1207          sayFor('posture.wontallow', where, nil);
1208  "\n";
1209          return nil;
1210      }
1211  /* Get on it first, if we are not on it already. */
1212      if (location.get(player) != where)
1213          contains.set(where, player);
1214      player.posture = want;
1215      noteDone(c, where);
1216      return nil;
1217  }
1218  
1219  standAction: Action
1220      exec(c) { return takePosture(c, c.dobj, standing); }
1221      report(c) { return accountDefault('posture.stand', c); }
1222  ;
1223  sitAction: Action
1224      exec(c) { return takePosture(c, c.dobj, sitting); }
1225      report(c) { return accountDefault('posture.sit', c); }
1226  ;
1227  lieAction: Action
1228      exec(c) { return takePosture(c, c.dobj, lying); }
1229      report(c) { return accountDefault('posture.lie', c); }
1230  ;
1231  standOnAction: Action
1232      needsDobj = true
1233      exec(c) { return takePosture(c, c.dobj, standing); }
1234      report(c) { return accountDefault('posture.stand', c); }
1235  ;
1236  sitOnAction: Action
1237      needsDobj = true
1238      exec(c) { return takePosture(c, c.dobj, sitting); }
1239      report(c) { return accountDefault('posture.sit', c); }
1240  ;
1241  lieOnAction: Action
1242      needsDobj = true
1243      exec(c) { return takePosture(c, c.dobj, lying); }
1244      report(c) { return accountDefault('posture.lie', c); }
1245  ;
1246  
1247  /* -------------------------------------------- verbs with words for objects */
1248  
1249  sayAction: Action
1250      exec(c)
1251      {
1252          local said = getLiteral(c);
1253          if (said == nil)
1254          {
1255              sayMsg('say.nothing', c);
1256  "\n";
1257              return nil;
1258          }
1259          sayAbout('say.ok', nil, nil, said);
1260  "\n";
1261          return nil;
1262      }
1263  ;
1264  
1265  writeOnAction: Action
1266      needsDobj = true
1267      preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1268      exec(c)
1269      {
1270          sayFor('writeon.cannot', c.dobj, nil);
1271  "\n";
1272          return nil;
1273      }
1274  ;
1275  
1276  /* THINK ABOUT X: a mere reference to something, which need not be present. */
1277  thinkAboutAction: Action
1278      needsDobj = true
1279      dobjIsTopic = true
1280      needsReach = nil
1281      exec(c)
1282      {
1283          sayFor('think.about', c.dobj, nil);
1284  "\n";
1285          return nil;
1286      }
1287  ;
1288  
1289  /*
1290   * LOOK UP X IN Y and CONSULT Y ABOUT X. The thing is the direct object and the
1291   * topic the indirect one, so a Consultable answers for itself.
1292   */
1293  consultAction: Action
1294      needsDobj = true
1295      needsIobj = true
1296      iobjIsTopic = true
1297      preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1298      exec(c)
1299      {
1300          local book = c.dobj;
1301          if (!book.isConsultable)
1302          {
1303              sayFor('consult.cannot', book, nil);
1304  "\n";
1305              return nil;
1306          }
1307          local entry = book.consult(c.iobj, getTopicText(c));
1308          if (entry == nil)
1309          {
1310              sayAbout('consult.nothing', book, nil, getTopicText(c));
1311  "\n";
1312              return nil;
1313          }
1314  "<<entry>>\n";
1315          return nil;
1316      }
1317  ;
1318  
1319  waitAction: Action
1320      exec(c) { sayMsg('wait.ok', c);"\n"; return nil; }
1321  ;
1322  
1323  examineAction: Action
1324      needsDobj = true
1325  /* Looking at something needs no more than seeing it. */
1326      needsReach = nil
1327      exec(c)
1328      {
1329          local item = c.dobj;
1330          item.seen = true;
1331          player.setKnowsAbout(item);
1332  /* Reading an emitting property displays it; there is nothing to test. */
1333          item.desc;
1334  "\n";
1335  /*
1336           * What is inside, when you can see inside. A glass jar shows its
1337           * contents with the lid on; a closed tin does not; and a RearContainer
1338           * says nothing, because what is behind it is out of sight until
1339           * someone looks there.
1340           */
1341          if (item.isContainer && item.contentsListed && seeInto(item))
1342          {
1343              local inside = [for held in contains.all(item) : held if !held.isHidden];
1344              if (inside.length() == 0)
1345                  {
1346                      sayFor('examine.empty', item, nil);
1347  "\n";
1348                  }
1349              else
1350              {
1351                  noteSeen(inside);
1352                  sayAbout(item.objPos == posOn ?'examine.onit' :'examine.init',
1353                           item, inside, nil);
1354  "\n";
1355              }
1356          }
1357          return nil;
1358      }
1359  ;
1360  
1361  takeAction: Action
1362      preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1363      needsDobj = true
1364  /* What you are already holding is the one you did not mean, and what is
1365       * bolted down is not a candidate for taking at all. */
1366      rank(item, role)
1367      {
1368          if (item.isFixed)
1369              return rankNever;
1370          if (carried(item))
1371              return rankAlready;
1372          return rankLogical;
1373      }
1374      exec(c)
1375      {
1376          local item = c.dobj;
1377          if (item == player)
1378          {
1379              sayMsg('take.self', c);
1380  "\n";
1381          }
1382          else if (carried(item))
1383          {
1384              sayMsg('take.already', c);
1385  "\n";
1386          }
1387          else if (item.isFixed)
1388          {
1389  /* A thing may say why in its own words; otherwise the library's. */
1390              if (item.cannotTakeMsg != nil)
1391  "<<item.cannotTakeMsg>>\n";
1392              else
1393              {
1394                  sayMsg('take.fixed', c);
1395  "\n";
1396              }
1397          }
1398          else
1399          {
1400              contains.set(player, item);
1401              noteDone(c, item);
1402          }
1403          return nil;
1404      }
1405      report(c) { return accountDefault('take.ok', c); }
1406  ;
1407  
1408  dropAction: Action
1409      preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
1410      needsDobj = true
1411  /* The mirror of take: only what you are holding can be dropped. */
1412      rank(item, role) { return carried(item) ? rankLogical : rankNotNow; }
1413      exec(c)
1414      {
1415          if (!carried(c.dobj))
1416          {
1417              sayMsg('drop.notheld', c);
1418  "\n";
1419          }
1420          else
1421          {
1422              contains.set(here(), c.dobj);
1423              noteDone(c, c.dobj);
1424          }
1425          return nil;
1426      }
1427      report(c) { return accountDefault('drop.ok', c); }
1428  ;
1429  
1430  openAction: Action
1431      preCond(item, role) { return role == dobjRole ? [touchObj, objUnlocked] : []; }
1432      needsDobj = true
1433      rank(item, role)
1434      {
1435          if (!item.isOpenable)
1436              return rankNever;
1437          return item.isOpen ? rankAlready : rankLogical;
1438      }
1439      exec(c)
1440      {
1441          local item = c.dobj;
1442          if (!item.isOpenable)
1443          {
1444              if (item.cannotOpenMsg != nil)
1445  "<<item.cannotOpenMsg>>\n";
1446              else
1447              {
1448                  sayMsg('open.cannot', c);
1449  "\n";
1450              }
1451          }
1452          else if (item.isLocked)
1453          {
1454              sayMsg('open.locked', c);
1455  "\n";
1456          }
1457          else if (item.isOpen)
1458          {
1459              sayMsg('open.already', c);
1460  "\n";
1461          }
1462          else
1463          {
1464              item.isOpen = true;
1465              noteDone(c, item);
1466  
1467              if (item.isContainer && item.contentsListed)
1468              {
1469                  local inside = [for held in contains.all(item) : held if !held.isHidden];
1470                  if (inside.length() > 0)
1471                  {
1472                      local r = mainReport(c,'open.reveals', item, nil);
1473                      if (r != nil)
1474                      {
1475                          r.list = inside;
1476                          r.reveals = inside;
1477                      }
1478                  }
1479              }
1480          }
1481          return nil;
1482      }
1483      report(c) { return accountDefault('open.ok', c); }
1484  ;
1485  
1486  closeAction: Action
1487      preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1488      needsDobj = true
1489      rank(item, role)
1490      {
1491          if (!item.isOpenable)
1492              return rankNever;
1493          return item.isOpen ? rankLogical : rankAlready;
1494      }
1495      exec(c)
1496      {
1497          local item = c.dobj;
1498          if (!item.isOpenable)
1499          {
1500              sayMsg('close.cannot', c);
1501  "\n";
1502          }
1503          else if (!item.isOpen)
1504          {
1505              sayMsg('close.already', c);
1506  "\n";
1507          }
1508          else
1509          {
1510              item.isOpen = nil;
1511              noteDone(c, item);
1512          }
1513          return nil;
1514      }
1515      report(c) { return accountDefault('close.ok', c); }
1516  ;
1517  
1518  /* TURN X, which the pencil sharpener needs. */
1519  turnAction: Action
1520      needsDobj = true
1521      exec(c) { sayMsg('turn.nothing', c);"\n"; return nil; }
1522  ;
1523  
1524  turnOnAction: Action
1525      needsDobj = true
1526      rank(item, role)
1527      {
1528          if (!item.isLightSource && !item.ofKind(Switch))
1529              return rankNever;
1530          return item.isLit || item.isOn ? rankAlready : rankLogical;
1531      }
1532      exec(c)
1533      {
1534          local item = c.dobj;
1535          if (!item.isLightSource)
1536          {
1537              sayMsg('turnon.cannot', c);
1538  "\n";
1539          }
1540          else if (item.isLit)
1541          {
1542              sayMsg('turnon.already', c);
1543  "\n";
1544          }
1545          else
1546          {
1547              item.isLit = true;
1548              noteDone(c, item);
1549          }
1550          return nil;
1551      }
1552  /* Seeing the room again is part of the account, not a second command. */
1553      report(c)
1554      {
1555          account('turnon.ok', c);
1556          if (!isDark())
1557              describeRoom();
1558          return nil;
1559      }
1560  ;
1561  
1562  turnOffAction: Action
1563      needsDobj = true
1564      rank(item, role)
1565      {
1566          if (!item.isLightSource && !item.ofKind(Switch))
1567              return rankNever;
1568          return item.isLit || item.isOn ? rankLogical : rankAlready;
1569      }
1570      exec(c)
1571      {
1572          local item = c.dobj;
1573          if (!item.isLightSource)
1574          {
1575              sayMsg('turnoff.cannot', c);
1576  "\n";
1577          }
1578          else if (!item.isLit)
1579          {
1580              sayMsg('turnoff.already', c);
1581  "\n";
1582          }
1583          else
1584          {
1585              item.isLit = nil;
1586              noteDone(c, item);
1587          }
1588          return nil;
1589      }
1590      report(c)
1591      {
1592          local done = c.done_;
1593          sayAbout('turnoff.ok', done[1], done, nil);
1594          if (isDark())
1595              sayMsg('light.out', c);
1596  "\n";
1597          return nil;
1598      }
1599  ;
1600  
1601  /* ASK X ABOUT Y and TELL X ABOUT Y. */
1602  converse(actor, about, asking)
1603  {
1604      if (!actor.isActor)
1605      {
1606          sayFor('talk.notactor', actor, nil);"\n";
1607          return nil;
1608      }
1609  
1610      if (actor.curState != nil && actor.curState.ofKind(HermitActorState))
1611      {
1612          actor.talkedThisTurn = true;
1613          if (actor.curState.hasNoResponse)
1614          {
1615              actor.curState.noResponse;
1616  "\n";
1617          }
1618          else
1619          {
1620              sayFor('talk.hermit', actor, nil);
1621  "\n";
1622          }
1623          return nil;
1624      }
1625      actor.talkedThisTurn = true;
1626      local t = topicFor(actor, about, asking);
1627      if (t == nil)
1628          {
1629              sayFor('talk.notopic', actor, nil);
1630  "\n";
1631          }
1632      else
1633      {
1634          deliverTopic(t, actor, about);
1635      }
1636      return nil;
1637  }
1638  
1639  /* Execute the exact selected entry; both ASK/TELL and menu adapters use it. */
1640  deliverTopic(t, actor, about)
1641  {
1642      if (t.id != nil) sayFor(t.id, actor, about);
1643      else t.reply;
1644  "\n";
1645      if (t.waits) hostAction(0);
1646      t.used = true;
1647      return nil;
1648  }
1649  
1650  askAboutAction: Action
1651      needsDobj = true
1652      needsIobj = true
1653      iobjIsTopic = true
1654      missingIobjMsg ='talk.vaguetopic'
1655      exec(c) { return converse(c.dobj, c.iobj, true); }
1656  ;
1657  
1658  tellAboutAction: Action
1659      needsDobj = true
1660      needsIobj = true
1661      iobjIsTopic = true
1662      missingIobjMsg ='talk.vaguetopic'
1663      exec(c) { return converse(c.dobj, c.iobj, nil); }
1664  ;
1665  
1666  eatAction: Action
1667      preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
1668      needsDobj = true
1669      rank(item, role) { return item.isEdible ? rankLogical : rankIllogical; }
1670      exec(c)
1671      {
1672          if (!c.dobj.isEdible)
1673          {
1674              sayFor('eat.cannot', c.dobj, nil);
1675  "\n";
1676              return nil;
1677          }
1678  
1679          contains.unset(location.get(c.dobj), c.dobj);
1680          noteDone(c, c.dobj);
1681          return nil;
1682      }
1683      report(c) { return account('eat.ok', c); }
1684  ;
1685  
1686  drinkAction: Action
1687      preCond(item, role) { return role == dobjRole ? [touchObj] : []; }
1688      needsDobj = true
1689      rank(item, role)
1690      {
1691          return item.isDrinkable || item.fluidName != nil ? rankLogical : rankIllogical;
1692      }
1693      exec(c)
1694      {
1695          if (!c.dobj.isDrinkable)
1696          {
1697              sayFor('drink.cannot', c.dobj, nil);
1698  "\n";
1699              return nil;
1700          }
1701          noteDone(c, c.dobj);
1702          return nil;
1703      }
1704      report(c) { return account('drink.ok', c); }
1705  ;
1706  
1707  pourIntoAction: Action
1708      preCond(item, role) { return role == dobjRole ? [objHeld] : [touchObj, objOpen]; }
1709      needsDobj = true
1710      needsIobj = true
1711      rank(item, role)
1712      {
1713          if (role == dobjRole)
1714              return item.fluidName != nil ? rankLogical : rankIllogical;
1715          return item.allowPourIntoMe ? rankLogical : rankUnlikely;
1716      }
1717      exec(c)
1718      {
1719          local from = c.dobj;
1720          if (from.fluidName == nil)
1721          {
1722              sayFor('pour.nothing', from, nil);
1723  "\n";
1724              return nil;
1725          }
1726          if (!c.iobj.allowPourIntoMe)
1727          {
1728              sayFor('pour.wontake', c.iobj, nil);
1729  "\n";
1730              return nil;
1731          }
1732  /* The fluid moves by being named somewhere else. */
1733          c.iobj.fluidName = from.fluidName;
1734          from.fluidName = nil;
1735          c.way_ = c.iobj.fluidName;
1736          noteDone(c, from);
1737          return nil;
1738      }
1739  /* The fluid's word and the thing it went into, so the account can name both. */
1740      report(c) { return accountAbout('pour.ok', c, c.way_); }
1741  ;
1742  
1743  detachPartAction: Action
1744      needsDobj = true
1745      rank(item, role) { return item.ofKind(Component) ? rankLogical : rankIllogical; }
1746      exec(c)
1747      {
1748          local part = c.dobj;
1749          if (!part.ofKind(Component) || !part.isDetachable)
1750          {
1751              sayFor('detach.fixed', part, nil);
1752  "\n";
1753              return nil;
1754          }
1755  /*
1756           * Detached, it is an ordinary thing: it stops being fixed and stops
1757           * being part of its owner's description, and it goes where the owner is
1758           * rather than staying inside it.
1759           */
1760          local owner = location.get(part);
1761          part.isFixed = nil;
1762          part.isDecoration = nil;
1763          part.isDetachable = nil;
1764          contains.set(owner == nil ? here() : location.get(owner), part);
1765          noteDone(c, part);
1766          return nil;
1767      }
1768      report(c) { return account('detach.part', c); }
1769  ;
1770  
1771  /*
1772   * GIVE X TO Y, and SHOW X TO Y.
1773   *
1774   * A library with actors and no way to hand one of them anything is a library
1775   * that has not met a game: the author game's whole ending is Becky giving Zoé
1776   * the tape. The default is a refusal, because what an actor does with a thing
1777   * is the game's business and not vhyl's — but the **action exists**, so a Doer
1778   * can catch it and a topic can answer it.
1779   */
1780  giveAction: Action
1781      preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
1782      needsDobj = true
1783      needsIobj = true
1784      rank(item, role) { return role == iobjRole && !item.isActor ? rankIllogical : rankLogical; }
1785      exec(c)
1786      {
1787          if (!c.iobj.isActor)
1788          {
1789              sayFor('give.notactor', c.iobj, nil);"\n";
1790              return nil;
1791          }
1792          if (!carried(c.dobj))
1793          {
1794              sayFor('drop.notheld', c.dobj, nil);"\n";
1795              return nil;
1796          }
1797          sayFor('give.refused', c.iobj, c.dobj);"\n";
1798          return nil;
1799      }
1800  ;
1801  
1802  showAction: Action
1803      preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
1804      needsDobj = true
1805      needsIobj = true
1806      rank(item, role) { return role == iobjRole && !item.isActor ? rankIllogical : rankLogical; }
1807      exec(c)
1808      {
1809          if (!c.iobj.isActor)
1810          {
1811              sayFor('give.notactor', c.iobj, nil);"\n";
1812              return nil;
1813          }
1814          sayFor('show.nothing', c.iobj, c.dobj);"\n";
1815          return nil;
1816      }
1817  ;
1818  
1819  unlockAction: Action
1820      preCond(item, role) { return role == dobjRole ? [touchObj] : [objHeld]; }
1821      needsDobj = true
1822      rank(item, role) { return item.isLockable ? rankLogical : rankIllogical; }
1823      exec(c)
1824      {
1825          local it = c.dobj;
1826          if (!it.isLockable)
1827          {
1828              sayFor('lock.cannot', it, nil);"\n";
1829              return nil;
1830          }
1831          if (!it.isLocked)
1832          {
1833              sayFor('unlock.already', it, nil);"\n";
1834              return nil;
1835          }
1836          local key = c.iobj != nil ? c.iobj : it.keyItem;
1837          if (it.keyItem == nil || key != it.keyItem || !carried(key))
1838          {
1839              sayFor('unlock.wrongkey', it, c.iobj);"\n";
1840              return nil;
1841          }
1842          it.isLocked = nil;
1843          c.iobj = key;
1844          noteDone(c, it);
1845          return nil;
1846      }
1847      report(c) { return accountDefault('unlock.ok', c); }
1848  ;
1849  
1850  lockAction: Action
1851      preCond(item, role) { return role == dobjRole ? [touchObj, objClosed] : [objHeld]; }
1852      needsDobj = true
1853      rank(item, role) { return item.isLockable ? rankLogical : rankIllogical; }
1854      exec(c)
1855      {
1856          local it = c.dobj;
1857          if (!it.isLockable)
1858          {
1859              sayFor('lock.cannot', it, nil);"\n";
1860              return nil;
1861          }
1862          if (it.isLocked)
1863          {
1864              sayFor('lock.already', it, nil);"\n";
1865              return nil;
1866          }
1867          local key = c.iobj != nil ? c.iobj : it.keyItem;
1868          if (it.keyItem == nil || key != it.keyItem || !carried(key))
1869          {
1870              sayFor('unlock.wrongkey', it, c.iobj);"\n";
1871              return nil;
1872          }
1873          it.isOpen = nil;
1874          it.isLocked = true;
1875          c.iobj = key;
1876          noteDone(c, it);
1877          return nil;
1878      }
1879      report(c) { return accountDefault('lock.ok', c); }
1880  ;
1881  
1882  attachAction: Action
1883      preCond(item, role) { return role == dobjRole ? [objHeld] : [touchObj]; }
1884      needsDobj = true
1885      needsIobj = true
1886      exec(c)
1887      {
1888          local a = c.dobj;
1889          local b = c.iobj;
1890          if (!a.isAttachable || !b.isAttachable)
1891              {
1892                  sayMsg('attach.cannot', c);
1893  "\n";
1894              }
1895          else if (a == b)
1896              {
1897                  sayMsg('put.initself', nil);
1898  "\n";
1899              }
1900          else if (a.attachesTo != nil && a.attachesTo != b)
1901              {
1902                  sayFor('attach.wrongpair', a, b);
1903  "\n";
1904              }
1905          else if (attachedTo.contains(a, b))
1906              {
1907                  sayMsg('attach.already', c);
1908  "\n";
1909              }
1910          else
1911          {
1912              attachedTo.set(a, b);
1913              noteDone(c, a);
1914          }
1915          return nil;
1916      }
1917      report(c) { return accountDefault('attach.ok', c); }
1918  ;
1919  
1920  detachAction: Action
1921      needsDobj = true
1922      needsIobj = true
1923      exec(c)
1924      {
1925          if (!attachedTo.contains(c.dobj, c.iobj))
1926              {
1927                  sayMsg('detach.notjoined', c);
1928  "\n";
1929              }
1930          else
1931          {
1932              attachedTo.unset(c.dobj, c.iobj);
1933              noteDone(c, c.dobj);
1934          }
1935          return nil;
1936      }
1937      report(c) { return accountDefault('detach.ok', c); }
1938  ;
1939  
1940  scoreAction: Action
1941      exec(c)
1942      {
1943          sayAbout('score.is', nil, nil, score.value);
1944  "\n";
1945          foreach (local why in score.awards)
1946  "  <<why>>\n";
1947          return nil;
1948      }
1949  ;
1950  
1951  senseAction: Action
1952      needsDobj = true
1953  /* Nor does listening to it, or smelling it. */
1954      needsReach = nil
1955      which = nil
1956      exec(c)
1957      {
1958          local it = c.dobj;
1959          if (it.ofKind(Sensed) && it.sense == which)
1960              it.desc;
1961          else if (which =='hear')
1962              sayMsg('listen.nothing', c);
1963          else
1964              sayMsg('smell.nothing', c);
1965  "\n";
1966          return nil;
1967      }
1968  ;
1969  
1970  listenAction: senseAction which ='hear';
1971  smellAction: senseAction which ='smell';
1972  
1973  flipAction: Action
1974      needsDobj = true
1975      exec(c)
1976      {
1977          local g = c.dobj;
1978          if (g.ofKind(Switch))
1979          {
1980              g.isOn = !g.isOn;
1981              c.way_ = g.isOn ?'flip.on' :'flip.off';
1982              noteDone(c, g);
1983          }
1984          else if (g.ofKind(Lever))
1985          {
1986              g.isUp = !g.isUp;
1987              c.way_ = g.isUp ?'pull.up' :'pull.down';
1988              noteDone(c, g);
1989          }
1990          else
1991              {
1992                  sayFor('flip.cannot', g, nil);
1993  "\n";
1994              }
1995          return nil;
1996      }
1997      report(c)
1998      {
1999          account(c.way_, c);
2000          c.dobj.activate();
2001          return nil;
2002      }
2003  ;
2004  
2005  pressAction: Action
2006      needsDobj = true
2007      exec(c)
2008      {
2009          if (!c.dobj.ofKind(Button))
2010              {
2011                  sayMsg('press.nothing', c);
2012  "\n";
2013              }
2014          else
2015              noteDone(c, c.dobj);
2016          return nil;
2017      }
2018  /*
2019       * The click, then whatever it set off. `activate` belongs here rather than
2020       * in `exec` because that is the order a player experiences it, and because
2021       * the account has to come first whatever the button does.
2022       */
2023      report(c)
2024      {
2025          account('press.ok', c);
2026          c.dobj.activate();
2027          return nil;
2028      }
2029  ;
2030  
2031  /* SET DIAL TO <word>. The word is a token, not an entity, so the rule binds it
2032   * as a part of speech and the action reads the text. */
2033  setToAction: Action
2034      needsDobj = true
2035      exec(c)
2036      {
2037          local d = c.dobj;
2038  /* The rule may have named the setting outright or captured whatever the
2039           * player typed. A captured token binds the word itself. */
2040          if (c.way_ == nil && c.set_ != nil)
2041              c.way_ = c.set_;
2042          if (!d.ofKind(Dial))
2043              {
2044                  sayFor('set.cannot', d, nil);
2045  "\n";
2046              }
2047          else if (!holds(d.validSettings, c.way_))
2048              {
2049                  sayFor('set.badvalue', d, nil);
2050  "\n";
2051              }
2052          else
2053          {
2054              d.setting = c.way_;
2055              noteDone(c, d);
2056          }
2057          return nil;
2058      }
2059  /*
2060       * The setting, then what setting it did. `way_` is the value here rather
2061       * than a message id, so this does not go through `account`.
2062       */
2063      report(c)
2064      {
2065          sayAbout('set.ok', c.dobj, nil, c.way_);
2066  "\n";
2067          c.dobj.activate();
2068          return nil;
2069      }
2070  ;
2071  
2072  boardAction: Action
2073      needsDobj = true
2074      exec(c) { return doBoard(c, c.dobj); }
2075  /*
2076       * A vehicle may describe boarding it in its own words. Read, not tested: an
2077       * emitting property prints when it is read, so `hasBoardDesc` is the flag.
2078       */
2079      report(c)
2080      {
2081          local item = c.done_[1];
2082          if (item.hasBoardDesc)
2083          {
2084              item.boardDesc;
2085  "\n";
2086              return nil;
2087          }
2088          return account('board.ok', c);
2089      }
2090  ;
2091  
2092  enterAction: Action
2093      needsDobj = true
2094      exec(c) { return doEnter(c, c.dobj); }
2095  /*
2096       * ENTER reaches BOARD when the thing is a vehicle, so it needs the same
2097       * account. Everything else about entering says its words as it happens: a
2098       * booth's `enterDesc` is the game's own prose and there is nothing to
2099       * summarise.
2100       */
2101      report(c) { return boardAction.report(c); }
2102  ;
2103  
2104  wearAction: Action
2105      preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
2106      needsDobj = true
2107      rank(item, role)
2108      {
2109          if (!item.isWearable)
2110              return rankNever;
2111          return item.isWorn ? rankAlready : rankLogical;
2112      }
2113      exec(c)
2114      {
2115          local item = c.dobj;
2116          if (!item.isWearable)
2117          {
2118              sayMsg('wear.cannot', c);
2119  "\n";
2120          }
2121          else if (item.isWorn)
2122          {
2123              sayMsg('wear.already', c);
2124  "\n";
2125          }
2126          else
2127          {
2128              contains.set(player, item);
2129              item.isWorn = true;
2130              noteDone(c, item);
2131          }
2132          return nil;
2133      }
2134      report(c) { return accountDefault('wear.ok', c); }
2135  ;
2136  
2137  doffAction: Action
2138      preCond(item, role) { return role == dobjRole ? [objHeld] : []; }
2139      needsDobj = true
2140      rank(item, role) { return item.isWorn ? rankLogical : rankNotNow; }
2141      exec(c)
2142      {
2143          if (!c.dobj.isWorn)
2144          {
2145              sayMsg('doff.notworn', c);
2146  "\n";
2147          }
2148          else
2149          {
2150              c.dobj.isWorn = nil;
2151              noteDone(c, c.dobj);
2152          }
2153          return nil;
2154      }
2155      report(c) { return accountDefault('doff.ok', c); }
2156  ;
2157  
2158  pushDirAction: Action
2159      needsDobj = true
2160      exec(c) { return doPush(c.dobj, c.dir_); }
2161  ;
2162  
2163  putOnAction: Action
2164      preCond(item, role) { return role == dobjRole ? [objHeld] : [touchObj]; }
2165      needsDobj = true
2166      needsIobj = true
2167      exec(c) { return doPutOn(c, c.dobj, c.iobj); }
2168  
2169      report(c) { return accountDefault(c.way_, c); }
2170  ;
2171  
2172  putInAction: Action
2173      preCond(item, role) { return role == dobjRole ? [objHeld] : [touchObj, objOpen]; }
2174      needsDobj = true
2175      needsIobj = true
2176      exec(c) { return doPutIn(c, c.dobj, c.iobj); }
2177      report(c) { return accountDefault(c.way_, c); }
2178  ;
2179  
2180  putBehindAction: Action
2181      needsDobj = true
2182      needsIobj = true
2183      exec(c)
2184      {
2185          local behind = c.iobj;
2186          if (behind.remapBehind == nil)
2187          {
2188              sayFor('putbehind.cannot', behind, nil);"\n";
2189              return nil;
2190          }
2191          contains.set(behind.remapBehind, c.dobj);
2192          noteDone(c, c.dobj);
2193          return nil;
2194      }
2195      report(c) { return accountDefault('putbehind.ok', c); }
2196  ;
2197  
2198  /*
2199   * LOOK BEHIND and LOOK UNDER. What is behind a thing is out of sight, so this
2200   * is how you find it — the leaflet behind the box and the note under the book.
2201   */
2202  lookUnderAction: Action
2203      needsDobj = true
2204      exec(c)
2205      {
2206          local it = c.dobj;
2207          local space = it.remapUnder;
2208          if (space == nil)
2209          {
2210              sayFor('lookunder.nothing', it, nil);"\n";
2211              return nil;
2212          }
2213          local found = contains.all(space);
2214          if (found.length() == 0)
2215              {
2216                  sayFor('lookunder.nothing', it, nil);
2217  "\n";
2218              }
2219          else
2220          {
2221              noteSeen(found);
2222              sayAbout('lookunder.found', it, found, nil);
2223  "\n";
2224              foreach (local held in contains.all(space))
2225                  held.isHidden = nil;
2226          }
2227          return nil;
2228      }
2229  ;
2230  
2231  lookBehindAction: Action
2232      needsDobj = true
2233      exec(c)
2234      {
2235          local it = c.dobj;
2236          local space = it.remapBehind != nil ? it.remapBehind : it;
2237          local found = contains.all(space);
2238          if (space == it && !it.isContainer)
2239              {
2240                  sayFor('lookbehind.nothing', it, nil);
2241  "\n";
2242              }
2243          else if (found.length() == 0)
2244              {
2245                  sayFor('lookbehind.nothing', it, nil);
2246  "\n";
2247              }
2248          else
2249          {
2250              noteSeen(found);
2251              sayAbout('lookbehind.found', it, found, nil);
2252  "\n";
2253  /* Found things stop being hidden. */
2254              foreach (local held in contains.all(space))
2255                  held.isHidden = nil;
2256          }
2257          return nil;
2258      }
2259  ;
2260  
2261  /* ------------------------------------------------------------- connectors */
2262  
2263  /** @api author
2264   * A traversable entity that can control staging, passage and travel reports. */
2265  class TravelConnector: Thing
2266      isFixed = true
2267  /* Where it leads. A Door works out its own, from the side you are on. */
2268      destination = nil
2269  /*
2270       * Said as you go through, if anything, and said as it is refused. These are
2271       * emitting properties: reading one prints it. So a game that supplies one
2272       * also sets the matching `has...` flag, because there is no way to ask
2273       * whether an emitting property is empty without printing it.
2274       */
2275      travelDesc = nil
2276      hasTravelDesc = nil
2277  
2278      canPass = true
2279      barrierDesc = nil
2280      hasBarrierDesc = nil
2281  
2282      onTravel = nil
2283  
2284      connectorStagingLocation = nil
2285  /* And the posture they have to be in once they are there. */
2286      connectorStagingPosture = standing
2287  ;
2288  
2289  /*
2290   * A way through that is not a door: an arch, a gap, a hole in a hedge. It is
2291   * `TravelConnector` with nothing added, which is the honest answer — a passage
2292   * differs from a door by not shutting, and a Door is the one that adds.
2293   */
2294  /** @api author
2295   * A travel connector without a door's open and closed state. */
2296  class Passage: TravelConnector;
2297  
2298  /** @api author
2299   * A passage whose bulk policy permits a path-like route. */
2300  class PathPassage: Passage
2301      bulkLimit = 0
2302  ;
2303  /** @api author
2304   * A connector representing travel upward. */
2305  class StairwayUp: TravelConnector;
2306  /** @api author
2307   * A connector representing travel downward. */
2308  class StairwayDown: TravelConnector;
2309  
2310  /** @api author
2311   * A travel connector shared by rooms through connects relation rows. */
2312  class Door: TravelConnector
2313      isOpenable = true
2314      isOpen = nil
2315  ;
2316  
2317  /** @api author
2318   * A door initially hidden from ordinary observation. */
2319  class SecretDoor: Door
2320      isHidden = true
2321  ;
2322  
2323  /*
2324   * Something you ride. Being in a vehicle is an ordinary containment row, so
2325   * travel carries whatever contains the player rather than the player alone —
2326   * no separate notion of a traveller is needed.
2327   */
2328  /** @api author
2329   * A boardable thing that can carry an actor through travel. */
2330  class Vehicle: Thing
2331      isVehicle = true
2332  /* You get **on** a bicycle, not in one. */
2333      objPos = posOn
2334      boardDesc = nil
2335      hasBoardDesc = nil
2336  /*
2337       * Whether it may go to this room. A method rather than a field, because the
2338       * answer is about where you are heading: asking about where you are would
2339       * let a bicycle into the hall as long as it started outdoors.
2340       */
2341      canEnter(room) { return true; }
2342  ;
2343  
2344  /* Too heavy to carry, but it can be shoved from room to room. */
2345  /** @api author
2346   * A thing too heavy for ordinary taking. */
2347  class Heavy: Thing
2348      isFixed = true
2349      canPushTravel = nil
2350  ;
2351  
2352  /*
2353   * Touchable, and not going anywhere. `Fixture` is scenery and belongs to the
2354   * room's description; an `Immovable` is a thing in its own right that simply
2355   * cannot be moved — a safe, an anvil, a fallen beam.
2356   */
2357  /** @api author
2358   * A thing that cannot be moved by ordinary actions. */
2359  class Immovable: Thing
2360      isFixed = true
2361      canPushTravel = nil
2362      refuses(action, c)
2363      {
2364          if (action == takeAction || action == pushDirAction)
2365              return msgFor('immovable.cannot', self, nil);
2366          return inherited(action, c);
2367      }
2368  ;
2369  
2370  /** @api author
2371   * A perceivable thing that cannot be touched. */
2372  class Intangible: Thing
2373      isFixed = true
2374      isDecoration = true
2375      isIntangible = true
2376      refuses(action, c)
2377      {
2378          if (action == examineAction || action == listenAction
2379              || action == smellAction || action == thinkAboutAction)
2380              return nil;
2381          return msgFor('intangible.cannot', self, nil);
2382      }
2383  ;
2384  
2385  /** @api author
2386   * A referable absence with a custom explanation. */
2387  class Unthing: Thing
2388      isFixed = true
2389      isDecoration = true
2390      refuses(action, c) { return msgFor('unthing.gone', self, nil); }
2391      rank(a, role) { return rankNever; }
2392  ;
2393  
2394  /* Part of the furniture: mentioned in the room description, not listed. */
2395  /** @api author
2396   * Fixed furniture or scenery within a location. */
2397  class Fixture: Thing
2398      isFixed = true
2399  ;
2400  
2401  /* Scenery you can look at but need not be told about. */
2402  /** @api author
2403   * Scenery normally omitted from room listings. */
2404  class Decoration: Fixture
2405      isDecoration = true
2406  
2407      isImportant = nil
2408      refuses(action, c)
2409      {
2410          if (isImportant)
2411              return inherited(action, c);
2412          if (action == examineAction || action == listenAction
2413              || action == smellAction || action == thinkAboutAction
2414              || action == askAboutAction || action == tellAboutAction
2415              || action == consultAction)
2416              return inherited(action, c);
2417          return msgFor('decoration.notimportant', self, nil);
2418      }
2419  ;
2420  
2421  /** @api author
2422   * A thing whose identity and placement are tied to another object. */
2423  class Component: Thing
2424      isFixed = true
2425  /* Part of its owner's description, not a separate line in the room. */
2426      isDecoration = true
2427  /* Whether it may be prised off. An AttachableComponent says yes. */
2428      isDetachable = nil
2429  /*
2430       * Taking a part says **what it is part of**, rather than the room's stock
2431       * refusal, because *the handle is part of the lantern* is the answer a
2432       * player actually wanted. `refuses` answers text, so this reads the message
2433       * rather than saying it.
2434       */
2435      refuses(action, c)
2436      {
2437  /* A detached part is an ordinary thing and refuses nothing. */
2438          if (action != takeAction || !isFixed)
2439              return nil;
2440          return msgFor('take.part', self, location.get(self));
2441      }
2442  /* And it is the one you did not mean when something else could be taken. */
2443      rank(a, role) { return a == takeAction ? rankSelf : rankLogical; }
2444  ;
2445  
2446  Component template'name';
2447  
2448  /** @api author
2449   * A component that can be attached to another entity. */
2450  class AttachableComponent: Component
2451      isDetachable = true
2452      isAttachable = true
2453  ;
2454  
2455  AttachableComponent template'name';
2456  
2457  /** @api author
2458   * Scenery visible but outside ordinary reach. */
2459  class Distant: Decoration
2460      isDistant = true
2461  ;
2462  
2463  Distant template'name';
2464  
2465  /* Things rest on top of it rather than inside. */
2466  /** @api author
2467   * A fixed surface on which things can rest. */
2468  class Surface: Fixture
2469      isContainer = true
2470      isOpenable = nil
2471  /* What a thing is said to be, relative to this. */
2472      objPos = posOn
2473  ;
2474  
2475  /* Holds things inside, and cannot be shut. */
2476  /** @api author
2477   * A Thing whose contents participate in containment and capacity rules. */
2478  class Container: Thing
2479      isContainer = true
2480      isOpenable = nil
2481      objPos = posIn
2482  ;
2483  
2484  /* A container with a lid. */
2485  /** @api author
2486   * A container that can be opened and closed. */
2487  class OpenableContainer: Container
2488      isOpenable = true
2489      isOpen = nil
2490  ;
2491  
2492  /* One that can be locked shut, and needs the right key. */
2493  /** @api author
2494   * An openable container with lock state. */
2495  class LockableContainer: OpenableContainer
2496      isLockable = true
2497      isLocked = true
2498  ;
2499  
2500  /* Things go behind it rather than in it. */
2501  /** @api author
2502   * A thing that receives objects placed behind it. */
2503  class RearContainer: Thing
2504      isContainer = true
2505      isOpenable = nil
2506      objPos = posBehind
2507  /* Behind is out of sight until you look there. */
2508      contentsListed = nil
2509  ;
2510  
2511  /** @api author
2512   * An openable container with a distinct bulk policy. */
2513  class BagOfHolding: OpenableContainer
2514      isOpen = true
2515      bulkCapacity = 100
2516  ;
2517  
2518  /** @api author
2519   * A subordinate physical part of another thing. */
2520  class SubComponent: Thing
2521      isFixed = true
2522      isContainer = true
2523  /* Named as part of the thing it belongs to, not on its own. */
2524      isDecoration = true
2525  ;
2526  
2527  SubComponent template'name';
2528  
2529  /** @api author
2530   * A surface on which an actor may stand. */
2531  class Platform: Surface
2532      isEnterable = true
2533      objPos = posOn
2534  /* What you can reach from up here. nil means everything in the room. */
2535      reachOut = true
2536  /* Said when you get on. An emitting string; a game overrides it. */
2537      enterDesc = nil
2538  ;
2539  
2540  /** @api author
2541   * A thing that can answer consult actions. */
2542  class Consultable: Thing
2543      isConsultable = true
2544      consult(about, words) { return nil; }
2545  ;
2546  
2547  /* A space you sit inside rather than on: a booth, a cupboard, a cart. */
2548  /** @api author
2549   * A container that an actor may enter. */
2550  class Booth: Container
2551      isEnterable = true
2552      isFixed = true
2553      objPos = posIn
2554      reachOut = true
2555      enterDesc = nil
2556      allowedPostures = [sitting, standing]
2557      defaultPosture = sitting
2558  ;
2559  
2560  /** @api author
2561   * A platform supporting sitting posture. */
2562  class Chair: Platform
2563      allowedPostures = [sitting, standing]
2564      defaultPosture = sitting
2565  ;
2566  
2567  /** @api author
2568   * A platform supporting lying posture. */
2569  class Bed: Platform
2570      allowedPostures = [lying, sitting, standing]
2571      defaultPosture = lying
2572  ;
2573  
2574  /* ------------------------------------------------------- attachment, lists */
2575  
2576  /** @api author
2577   * A thing that can attach to another entity. */
2578  class Attachable: Thing
2579      isAttachable = true
2580  /* What it will join to. nil means anything attachable. */
2581      attachesTo = nil
2582  ;
2583  
2584  Attachable template'name';
2585  
2586  /** @api author
2587   * Cycles, stops or exhausts an authored list of values. */
2588  class EventList: object
2589      items = []
2590      at = 0
2591  /* 'cycle' starts again, 'stop' stays on the last, 'once' then silence. */
2592      kind ='cycle'
2593      next()
2594      {
2595          local count = items.length();
2596          if (count == 0)
2597              return nil;
2598          at = at + 1;
2599          if (at > count)
2600          {
2601              if (kind =='cycle')
2602                  at = 1;
2603              else if (kind =='stop')
2604                  at = count;
2605              else
2606                  return nil;
2607          }
2608          return items[at];
2609      }
2610  ;
2611  
2612  /** @api author
2613   * Tracks a condition-driven interval in the world. */
2614  class Scene: object
2615      isHappening = nil
2616      hasHappened = nil
2617  /* Answered true when it should start. */
2618      startsWhen = nil
2619  /* Answered true when it should stop. */
2620      endsWhen = nil
2621      whenStarting() { return nil; }
2622      whenEnding() { return nil; }
2623  ;
2624  
2625  score: object
2626      value = 0
2627      awards = []
2628  ;
2629  
2630  /* Award points once for a named achievement. */
2631  awardPoints(points, why)
2632  {
2633      if (holds(score.awards, why))
2634          return nil;
2635      score.awards = score.awards + [why];
2636      score.value = score.value + points;
2637      sayAbout(points == 1 ?'score.gained1' :'score.gained', nil, nil, points);
2638  "\n";
2639      return nil;
2640  }
2641  
2642  /* Run every turn: start and stop whatever scenes are due. */
2643  runScenes()
2644  {
2645      for (local sc = firstObj(Scene); sc != nil; sc = nextObj(sc, Scene))
2646      {
2647          if (!sc.isHappening && !sc.hasHappened && sc.startsWhen)
2648          {
2649              sc.isHappening = true;
2650              sc.whenStarting();
2651          }
2652          else if (sc.isHappening && sc.endsWhen)
2653          {
2654              sc.isHappening = nil;
2655              sc.hasHappened = true;
2656              sc.whenEnding();
2657          }
2658      }
2659      return nil;
2660  }
2661  
2662  /* --------------------------------------------------------------- sensory */
2663  
2664  /** @api author
2665   * A sensed phenomenon that can be present in multiple rooms. */
2666  class Sensed: MultiLoc
2667  /* Mentioned when the room is described, if it is worth mentioning. */
2668      isAmbient = true
2669      ambientDesc =""
2670  ;
2671  
2672  /** @api author
2673   * A sound present in one or more rooms. */
2674  class Noise: Sensed
2675      sense ='hear'
2676  ;
2677  
2678  /** @api author
2679   * A smell present in one or more rooms. */
2680  class Odour: Sensed
2681      sense ='smell'
2682  ;
2683  
2684  Sensed template'name';
2685  Noise template'name';
2686  Odour template'name';
2687  
2688  /* What can be sensed here, listed after the room. */
2689  listSensed(room)
2690  {
2691      for (local n = firstObj(Sensed); n != nil; n = nextObj(n, Sensed))
2692      {
2693          if (n.isAmbient && presentIn.contains(n, room))
2694          {
2695              n.ambientDesc;
2696  " ";
2697          }
2698      }
2699      return nil;
2700  }
2701  
2702  /* ----------------------------------------------------------------- actors */
2703  
2704  /* --------------------------------------------------------- actor travel */
2705  
2706  travelActions: object
2707  /* One command epoch; scalar stamps avoid retaining a fresh world list
2708       * every turn. Undo/rollback journal the epoch and actor stamps together. */
2709      cycle = 0
2710      backgroundActor = nil
2711  /* The actor a `sayDeparting` is currently being asked about. */
2712      conn = nil
2713  ;
2714  
2715  /** @api author
2716   * A character that participates in travel, conversation and actor states. */
2717  class Actor: Thing
2718      travelCycle = -1
2719  
2720      /** @api author
2721       * Decide whether this actor may accompany a leader along this journey. */
2722      canAccompanyTravel(leader, conn, dest) { return true; }
2723  
2724      presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
2725                      &isLightSource, &isFixed, &isDecoration, &isTransparent,
2726                      &isHidden, &bulk, &bulkCapacity, &objPos,
2727                      &curState]
2728      isFixed = true
2729      isActor = true
2730  /* The state it is in, which decides what it says and does. */
2731      /** @api author
2732       * Current ActorState, selected by the library or setCurState. */
2733      curState = nil
2734  /* Said instead of desc while in a state that has its own. */
2735      desc = nil
2736  /* Whether this actor has spoken this turn, for a ConvAgendaItem. */
2737      talkedThisTurn = nil
2738  
2739      posture = standing
2740  
2741      /** @api author
2742       * Change actor state, invoking deactivation and activation hooks. */
2743      setCurState(state)
2744      {
2745          if (state == curState)
2746              return nil;
2747          local old = curState;
2748          if (old != nil)
2749              old.deactivateState(self, state);
2750          curState = state;
2751          if (state != nil)
2752          {
2753              state.actor = self;
2754              state.activateState(self, old);
2755          }
2756          return nil;
2757      }
2758  
2759  /* Background activity for an actor with no state of its own to run one. */
2760      /** @api author
2761       * Run background behaviour when the actor has no state turn hook. */
2762      idleTurn() { return nil; }
2763  
2764  /* Whether this actor will do as it is told. The state decides. */
2765      obeyCommand(issuer, a)
2766      {
2767          return curState == nil ? nil : curState.obeyCommand(issuer, a);
2768      }
2769  
2770      moveIntoForTravel(room)
2771      {
2772          if (room != nil)
2773              relocateActor(self, room);
2774          return nil;
2775      }
2776  
2777      travelTo(dest, conn)
2778      {
2779          if (dest == nil || contains.outermost(self) == dest
2780              || !canStartJourney())
2781              return nil;
2782          local from = contains.outermost(self);
2783          local escort = accompanists(self, from, conn, dest);
2784          if (escort == nil)
2785              return nil;
2786          return travelParty(dest, conn, escort);
2787      }
2788  
2789  /* A companion's own background agenda can still speak or do other work.
2790       * Its travel waits for its leader, independent of declaration order. */
2791      canStartJourney()
2792      {
2793          if (travelCycle == travelActions.cycle)
2794              return nil;
2795          if (travelActions.backgroundActor == self && curState != nil
2796              && (curState.accompanyingActor != nil || curState.escortActor != nil))
2797              return nil;
2798          return true;
2799      }
2800  
2801  /* Internal: eligibility has been checked exactly once for this journey. */
2802      travelParty(dest, conn, escort)
2803      {
2804  
2805          travelCycle = travelActions.cycle;
2806          foreach (local a in escort)
2807              a.travelCycle = travelActions.cycle;
2808          foreach (local a in escort)
2809          {
2810              wearTravelState(a, conn);
2811              a.travelAlone(dest, conn);
2812          }
2813          travelAlone(dest, conn);
2814          finishAccompaniment(escort, true);
2815          return true;
2816      }
2817  
2818  /* Internal: the party has already been gathered and admitted. */
2819      travelAlone(dest, conn)
2820      {
2821          local from = contains.outermost(self);
2822          local way = wayFrom(from, dest);
2823          foreach (local item in contains.all(from))
2824              item.beforeTravel(self, conn);
2825          if (curState != nil)
2826              curState.beforeTravel(self, conn);
2827  
2828          if (from == here() && from != dest)
2829              sayDeparting(way, conn);
2830          relocateActor(self, dest);
2831          if (conn != nil && conn.onTravel != nil)
2832              conn.onTravel;
2833          if (dest == here() && from != dest)
2834              sayArriving(way == nil ? nil : oppositeOf(way), conn);
2835          foreach (local item in contains.all(dest))
2836              item.afterTravel(self, conn);
2837          if (curState != nil)
2838              curState.afterTravel(self, conn);
2839          return nil;
2840      }
2841  
2842      scriptedTravelTo(dest)
2843      {
2844          if (dest == nil || !canStartJourney())
2845              return nil;
2846          local from = contains.outermost(self);
2847          local way = wayFrom(from, dest);
2848          if (way == nil)
2849              return nil;
2850          local conn = exits.get(from, way);
2851          if (conn != nil && conn.ofKind(TravelConnector) && !conn.canPass)
2852              return nil;
2853          local escort = accompanists(self, from, conn, dest);
2854          if (escort == nil)
2855              return nil;
2856          if (conn != nil && conn.ofKind(Door) && !conn.isOpen)
2857          {
2858              if (conn.isLocked)
2859                  return nil;
2860              conn.isOpen = true;
2861  /*
2862               * A door stands in both the rooms it joins, so the player sees it
2863               * open from either side — which is what `connectorHere` answers and
2864               * what plain visibility, which is about containment, does not.
2865               */
2866              if (connectorHere(conn) || visible(conn))
2867              {
2868                  sayFor('travel.opens', self, conn);
2869  "\n";
2870              }
2871          }
2872          if (conn != nil && conn.ofKind(TravelConnector) && !conn.canPass)
2873              return nil;
2874          return travelParty(dest, conn, escort);
2875      }
2876  
2877  /* What the player sees when this actor leaves, and when one arrives. */
2878      sayDeparting(way, conn)
2879      {
2880  /* A state may take the message over, which is how accompanying travel
2881           * says *the lead guard prods you on* instead of *the guard leaves*. */
2882          if (curState != nil && curState.saysDeparting)
2883              return curState.sayDeparting(self, way, conn);
2884          sayAbout(way == nil ?'travel.leaves' :'travel.leavesway',
2885                   self, nil, way);
2886  "\n";
2887          return nil;
2888      }
2889  
2890      sayArriving(fromWay, conn)
2891      {
2892          sayAbout(fromWay == nil ?'travel.arrives' :'travel.arrivesway',
2893                   self, nil, fromWay);
2894  "\n";
2895          return nil;
2896      }
2897  ;
2898  
2899  /** @api author
2900   * Behaviour and presentation state associated with an Actor. */
2901  class ActorState: object
2902      presentation = [&actor, &accompanyingActor, &escortActor, &escortDest]
2903      /** @api author
2904       * Actor to which this state belongs. */
2905      actor = nil
2906      accompanyingActor = nil
2907      escortActor = nil
2908      escortDest = nil
2909  /* Mentioned when the actor is described. */
2910      /** @api author
2911       * Description emitted while the actor is in this state. */
2912      stateDesc =""
2913  /* Only these topics answer while in this state; nil means all of them. */
2914      /** @api author
2915       * Topics available in this state, or nil for all matching topics. */
2916      only = nil
2917  
2918      /** @api author
2919       * Prefer this state during initial state selection. */
2920      isInitState = nil
2921  
2922      specialDesc = nil
2923  /* Whether `specialDesc` says anything; an emitting property cannot be asked. */
2924      hasSpecialDesc = nil
2925  
2926      /** @api author
2927       * Decide whether this actor accepts an issued command. */
2928      obeyCommand(issuer, a) { return nil; }
2929  /* Reacting to what happens, with the state rather than with the actor. */
2930      /** @api author
2931       * React before a resolved action is performed. */
2932      beforeAction(c) { return nil; }
2933      /** @api author
2934       * React after a resolved action is performed. */
2935      afterAction(c) { return nil; }
2936      /** @api author
2937       * React before the actor travels. */
2938      beforeTravel(traveller, conn) { return nil; }
2939      /** @api author
2940       * React after the actor travels. */
2941      afterTravel(traveller, conn) { return nil; }
2942  /* Called as this state becomes, and stops being, the actor's. */
2943      /** @api author
2944       * Run when this state becomes active for the actor. */
2945      activateState(a, oldState) { return nil; }
2946      /** @api author
2947       * Run when this state stops being active for the actor. */
2948      deactivateState(a, newState) { return nil; }
2949  
2950      /** @api author
2951       * Run this state's background activity on a completed turn. */
2952      takeTurn() { return nil; }
2953  /* Whether this state takes the departure message over; see below. */
2954      saysDeparting = nil
2955      sayDeparting(a, way, conn) { return nil; }
2956  /* Whether this state travels with somebody; see `AccompanyingState`. */
2957      accompanyTravel(leadActor, conn) { return nil; }
2958      getAccompanyingTravelState(leadActor, conn) { return nil; }
2959  ;
2960  
2961  /** @api author
2962   * An actor state that declines ordinary conversation. */
2963  class HermitActorState: ActorState
2964      noResponse = nil
2965  
2966      hasNoResponse = nil
2967  /* Nothing gets through, so no topic does either. */
2968      only = []
2969  ;
2970  
2971  /** @api author
2972   * An ActorState that follows an explicitly stored leader during travel. */
2973  class AccompanyingState: ActorState
2974  /* The stored leader is authoritative and inspectable. The hook may
2975       * decline a particular journey, but cannot choose a different leader. */
2976      /** @api author
2977       * Actor followed by this state's actor. */
2978      accompanyingActor = player
2979      accompanyTravel(leadActor, conn) { return true; }
2980  /* The state to wear for the journey itself. */
2981      getAccompanyingTravelState(leadActor, conn) { return nil; }
2982  ;
2983  
2984  /** @api author
2985   * Temporary state used while an actor follows a leader through travel. */
2986  class AccompanyingInTravelState: ActorState
2987      saysDeparting = true
2988  /* Where to go back to once the journey is over. */
2989      stateAfter = nil
2990      sayDeparting(a, way, conn)
2991      {
2992          sayFor('travel.comeswith', a, nil);
2993  "\n";
2994          return nil;
2995      }
2996  ;
2997  
2998  /** @api author
2999   * An accompanying state that leads another actor toward a tour destination. */
3000  class GuidedTourState: AccompanyingState
3001      accompanyingActor = nil
3002      escortActor = player
3003  /* The room this leg of the tour leads to. */
3004      escortDest = nil
3005  /* The state for the next stop. */
3006      stateAfterEscort = nil
3007  /* A tour leads rather than follows, so it does not wait to be taken. */
3008      accompanyTravel(leadActor, conn) { return nil; }
3009  /* And it goes first, which is what the guide is for. */
3010      saysDeparting = true
3011      sayDeparting(a, way, conn)
3012      {
3013          sayFor('travel.leadsway', a, nil);
3014  "\n";
3015          return nil;
3016      }
3017  ;
3018  
3019  /*
3020   * One thing an actor will talk about. Matched by the entity the player named,
3021   * so ASK SAILOR ABOUT LIGHTHOUSE finds the entry whose matchObj is the
3022   * lighthouse — topics are entities, which the vocabulary already resolves.
3023   */
3024  
3025  /** @api author
3026   * A vocabulary-resolved subject used by ask and tell dialogue. */
3027  class Topic: Thing
3028      isFamiliar = true
3029      isDecoration = true
3030      isFixed = true
3031  ;
3032  
3033  /* And a Distant says *too far off*, which the reach check answers first. */
3034  
3035  /** @api author
3036   * An ask or tell response matched to a topic and optionally an actor state. */
3037  class TopicEntry: object
3038      /** @api author
3039       * Actor who offers this topic response. */
3040      actor = nil
3041      /** @api author
3042       * Topic or object matched by this entry. */
3043      matchObj = nil
3044  /* Only offered while the actor is in this state; nil means always. */
3045      /** @api author
3046       * Restrict the entry to this actor state, or nil for any state. */
3047      inState = nil
3048      /** @api author
3049       * Match ask exchanges when true and tell exchanges otherwise. */
3050      isAsk = true
3051  
3052      /** @api author
3053       * Message id used for the response. */
3054      id = nil
3055  /* Or the words themselves, for a game that only ever runs in a terminal. */
3056      /** @api author
3057       * Literal response text for a terminal-only game. */
3058      reply = nil
3059  
3060      /** @api author
3061       * Advance a turn after delivering the topic response. */
3062      waits = nil
3063  
3064      /** @api author
3065       * Whether this entry currently participates in topic matching. */
3066      isActive = true
3067  /* Said once, then this entry steps aside for the next. */
3068      /** @api author
3069       * Retire this topic entry after its first accepted exchange. */
3070      once = nil
3071      /** @api author
3072       * Library-maintained record of whether this entry was accepted. */
3073      used = nil
3074  ;
3075  
3076  /* ----------------------------------------------------------------- states */
3077  
3078  /** @api author
3079   * Synchronises vocabulary with an observed property condition. */
3080  class State: object
3081  /* Whose condition this is. */
3082      item = nil
3083  
3084      appliesTo = nil
3085  /* And a further condition, for a class only some of whose instances have it. */
3086      applies(it) { return true; }
3087  /* The property it watches, as an address: `&isSharpened`. */
3088      watches = nil
3089  /* The adjective it answers to when that property is true, and when not.
3090       * Either may be nil, for a condition only one side of which has a word. */
3091      whenTrue = nil
3092      whenFalse = nil
3093  /* What it last wrote, so a change takes the old word away again. */
3094      said = nil
3095  /* The same, per thing, when the state covers a whole class. */
3096      saidFor = []
3097      saidItems = []
3098  /* What this state last wrote for one thing. */
3099      wordFor(it)
3100      {
3101          local at = saidItems.indexOf(it);
3102          return at == nil ? nil : saidFor[at];
3103      }
3104      noteWord(it, word)
3105      {
3106          local at = saidItems.indexOf(it);
3107          if (at == nil)
3108          {
3109              saidItems = saidItems + [it];
3110              saidFor = saidFor + [word];
3111              return nil;
3112          }
3113          local rebuilt = [];
3114          for (local i = 1; i <= saidFor.length(); ++i)
3115              rebuilt = rebuilt + [i == at ? word : saidFor[i]];
3116          saidFor = rebuilt;
3117          return nil;
3118      }
3119  /* The adjective this state gives that thing as it stands, or nil. */
3120      wordNow(it)
3121      {
3122          if (watches == nil)
3123              return nil;
3124          return it.(watches) ? whenTrue : whenFalse;
3125      }
3126  ;
3127  
3128  /*
3129   * Bring every state's vocabulary into line with the world.
3130   *
3131   * Idempotent: a state whose word has not changed writes nothing, so this costs
3132   * one property read per state on a turn where nothing happened.
3133   */
3134  syncStates()
3135  {
3136      for (local s = firstObj(State); s != nil; s = nextObj(s, State))
3137      {
3138          if (s.watches == nil)
3139              continue;
3140  /* One thing's own condition. */
3141          if (s.item != nil)
3142          {
3143              local wanted = s.wordNow(s.item);
3144              if (wanted == s.said)
3145                  continue;
3146              if (s.said != nil)
3147                  vocab.unset(s.item, s.said, &adjective);
3148              if (wanted != nil)
3149                  vocab.set(s.item, wanted, &adjective);
3150              s.said = wanted;
3151              continue;
3152          }
3153  /* Or every instance of a class that the state applies to. */
3154          if (s.appliesTo == nil)
3155              continue;
3156          for (local it = firstObj(s.appliesTo); it != nil;
3157               it = nextObj(it, s.appliesTo))
3158          {
3159              if (!s.applies(it))
3160                  continue;
3161              local wanted = s.wordNow(it);
3162              local was = s.wordFor(it);
3163              if (wanted == was)
3164                  continue;
3165              if (was != nil)
3166                  vocab.unset(it, was, &adjective);
3167              if (wanted != nil)
3168                  vocab.set(it, wanted, &adjective);
3169              s.noteWord(it, wanted);
3170          }
3171      }
3172      return nil;
3173  }
3174  
3175  /** @api author
3176   * A scheduled item of actor behaviour. */
3177  class AgendaItem: Daemon
3178  /* Only runs while the actor is in this state. */
3179      inState = nil
3180      actor = nil
3181  
3182      isReady = true
3183      isDone = nil
3184  
3185      invokeItem() { return nil; }
3186  
3187      offstage = nil
3188      isDue
3189      {
3190          if (isDone)
3191              return nil;
3192          if (!isReady)
3193              return nil;
3194          if (inState != nil && (actor == nil || actor.curState != inState))
3195              return nil;
3196          if (!offstage && (actor == nil || !visible(actor)))
3197              return nil;
3198          return true;
3199      }
3200  ;
3201  
3202  /** @api author
3203   * An agenda item that accounts for conversation activity. */
3204  class ConvAgendaItem: AgendaItem
3205      isReady = (actor != nil && !actor.talkedThisTurn)
3206  ;
3207  
3208  addToAgenda(a, item)
3209  {
3210      if (item == nil)
3211          return nil;
3212      item.actor = a;
3213      item.isActive = true;
3214      item.isDone = nil;
3215      return nil;
3216  }
3217  
3218  removeFromAgenda(a, item)
3219  {
3220      if (item != nil)
3221          item.isActive = nil;
3222      return nil;
3223  }
3224  
3225  topicAvailableTo(t, asker)
3226  {
3227      return t.isActive && asker.knowsAbout(t.matchObj)
3228          && (t.inState == nil || t.actor.curState == t.inState)
3229          && !(t.once && t.used);
3230  }
3231  
3232  topicFor(actor, about, asking)
3233  {
3234      for (local t = firstObj(TopicEntry); t != nil; t = nextObj(t, TopicEntry))
3235      {
3236          if (t.actor != actor || t.matchObj != about)
3237              continue;
3238          if (!topicAvailableTo(t, player))
3239              continue;
3240          if (t.isAsk != asking)
3241              continue;
3242          if (t.inState != nil && actor.curState != t.inState)
3243              continue;
3244          if (t.once && t.used)
3245              continue;
3246          return t;
3247      }
3248      return nil;
3249  }
3250  
3251  /* ---------------------------------------------------------------- gadgets */
3252  
3253  /** @api author
3254   * Base for manipulable controls such as switches and buttons. */
3255  class Gadget: Thing
3256      isFixed = true
3257  /* What turning, pushing or flipping it does. */
3258      activate() { return nil; }
3259  ;
3260  
3261  /* On or off. */
3262  /** @api author
3263   * A binary control operated by turn-on and turn-off actions. */
3264  class Switch: Gadget
3265  
3266      presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
3267                      &isLightSource, &isFixed, &isDecoration, &isTransparent,
3268                      &isHidden, &bulk, &bulkCapacity, &objPos,
3269                      &isOn]
3270      isOn = nil
3271      desc = nil
3272  ;
3273  
3274  /* Up or down. */
3275  /** @api author
3276   * A control with lever-specific action reporting. */
3277  class Lever: Gadget
3278  
3279      presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
3280                      &isLightSource, &isFixed, &isDecoration, &isTransparent,
3281                      &isHidden, &bulk, &bulkCapacity, &objPos,
3282                      &isUp]
3283      isUp = true
3284      desc = nil
3285  ;
3286  
3287  /* Set to one of several positions. */
3288  /** @api author
3289   * A control whose setting can be changed by the player. */
3290  class Dial: Gadget
3291  
3292      presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
3293                      &isLightSource, &isFixed, &isDecoration, &isTransparent,
3294                      &isHidden, &bulk, &bulkCapacity, &objPos,
3295                      &setting]
3296      setting = nil
3297      validSettings = []
3298      desc = nil
3299  ;
3300  
3301  /* Pressed, and does whatever it does. */
3302  /** @api author
3303   * A control operated by pressing. */
3304  class Button: Gadget
3305      desc = nil
3306  ;
3307  
3308  Actor template'name';
3309  Gadget template'name';
3310  Switch template'name';
3311  Lever template'name';
3312  Dial template'name';
3313  Button template'name';
3314  
3315  /** @api author
3316   * A thing that can be eaten. */
3317  class Food: Thing
3318      isEdible = true
3319  ;
3320  
3321  Food template'name';
3322  
3323  /* Worn rather than merely carried. */
3324  /** @api author
3325   * A thing that can be worn and removed. */
3326  class Wearable: Thing
3327  
3328      presentation = [&name, &isOpen, &isOpenable, &isContainer, &isLit,
3329                      &isLightSource, &isFixed, &isDecoration, &isTransparent,
3330                      &isHidden, &bulk, &bulkCapacity, &objPos,
3331                      &isWorn]
3332      isWearable = true
3333      isWorn = nil
3334  ;
3335  
3336  /*
3337   * Something you go into, which takes you somewhere. The oak you climb and the
3338   * chute you slide down are both this.
3339   */
3340  /** @api author
3341   * A fixed object an actor can enter. */
3342  class Enterable: Fixture
3343      isEnterable = true
3344      destination = nil
3345  /* An emitting string, like desc. */
3346      enterDesc = nil
3347  ;
3348  
3349  /*
3350   * In several rooms at once. Its rows are in presentIn rather than contains,
3351   * because containment answers one container and this thing has no one room.
3352   */
3353  /** @api author
3354   * A fixture present in multiple rooms through presentIn relation rows. */
3355  class MultiLoc: Fixture
3356      isMultiLoc = true
3357  
3358      locationList = []
3359  ;
3360  
3361  TravelConnector template'name';
3362  Passage template'name';
3363  PathPassage template'name';
3364  StairwayUp template'name';
3365  StairwayDown template'name';
3366  Door template'name';
3367  SecretDoor template'name';
3368  Vehicle template'name';
3369  Heavy template'name';
3370  Fixture template'name';
3371  Decoration template'name';
3372  Surface template'name';
3373  Container template'name';
3374  OpenableContainer template'name';
3375  LockableContainer template'name';
3376  RearContainer template'name';
3377  BagOfHolding template'name';
3378  Platform template'name';
3379  Booth template'name';
3380  Wearable template'name';
3381  Enterable template'name';
3382  MultiLoc template'name';
3383  
3384  class PreinitObject: object execBeforeMe = [];
3385  
3386  /*
3387   * A MultiLoc names the rooms it stands in and vhyl writes the rows, for the
3388   * same reason a door does: where a thing is belongs to the declaration, and
3389   * turning that into table rows is the library's job.
3390   */
3391  multiLocPreinit: PreinitObject
3392      execute()
3393      {
3394          for (local m = firstObj(MultiLoc); m != nil; m = nextObj(m, MultiLoc))
3395          {
3396              foreach (local room in m.locationList)
3397                  presentIn.set(m, room);
3398          }
3399          return nil;
3400      }
3401  ;
3402  
3403  /* The room on a door's far side, from the one you are standing in. */
3404  doorDestination(door, from)
3405  {
3406      foreach (local side in connects.all(door))
3407      {
3408          if (side != from)
3409              return side;
3410      }
3411      return nil;
3412  }
3413  
3414  /* A connector is in scope from either of the rooms it joins. */
3415  connectorHere(item)
3416  {
3417      if (!connects.contains(item, here()))
3418          return nil;
3419      return !item.isHidden;
3420  }
3421  
3422  /* ---------------------------------------------------------- action helpers */
3423  
3424  doInventory()
3425  {
3426      local held = [for item in contains.all(player) : item.name];
3427      if (held.length() == 0)
3428          {
3429              sayMsg('inventory.none', nil);
3430  "\n";
3431          }
3432      else
3433      {
3434          sayMsg('inventory.some', nil);
3435          foreach (local item in contains.all(player))
3436              {
3437  "\n  ";
3438                  sayFor(item.isWorn ?'inventory.itemworn' :'inventory.item',
3439                         item, nil);
3440              }
3441  "\n";
3442      }
3443      return nil;
3444  }
3445  
3446  doGo(way)
3447  {
3448  
3449      if (way == outward && nestedIn() != nil)
3450          return doGetOff();
3451      local target = exits.get(here(), way);
3452  
3453      if (target != nil && target.isHidden)
3454          target = nil;
3455      if (target == nil)
3456      {
3457          sayMsg('go.nowhere', nil);
3458  "\n";
3459          return nil;
3460      }
3461      local to = target;
3462      if (target.ofKind(TravelConnector))
3463      {
3464  
3465          local stage = target.connectorStagingLocation;
3466          if (stage != nil)
3467          {
3468              if (location.get(player) != stage)
3469              {
3470                  if (!tryImplied(verbForPosture(target.connectorStagingPosture),
3471                                  stage, nil, nil))
3472                  {
3473                      sayFor('stage.notthere', stage, nil);
3474  "\n";
3475                      return nil;
3476                  }
3477              }
3478              else if (player.posture != target.connectorStagingPosture)
3479              {
3480                  player.posture = target.connectorStagingPosture;
3481              }
3482          }
3483  
3484          if (target.isOutOfReach && !target.canObjReachSelf(player))
3485          {
3486              sayFor('reach.toohigh', target, nil);"\n";
3487              return nil;
3488          }
3489          if (target.ofKind(Door) && !target.isOpen)
3490          {
3491  
3492              if (!tryImplied(23, target, nil, nil))
3493              {
3494                  sayFor('go.shut', target, nil);"\n";
3495                  return nil;
3496              }
3497          }
3498          if (!target.canPass)
3499          {
3500              if (target.barrierDesc != nil)
3501  "<<target.barrierDesc>>\n";
3502              else
3503                  {
3504                      sayFor('go.barred', target, nil);
3505  "\n";
3506                  }
3507              return nil;
3508          }
3509  
3510          if (target.bulkLimit > 0 && carriedBulk() > target.bulkLimit)
3511          {
3512              sayFor('go.toobulky', target, nil);
3513  "\n";
3514              return nil;
3515          }
3516          if (target.hasTravelDesc)
3517          {
3518              target.travelDesc;
3519  "\n";
3520          }
3521          to = target.ofKind(Door) ? doorDestination(target, here()) : target.destination;
3522          if (to == nil)
3523          {
3524              sayFor('go.leadsnowhere', target, nil);"\n";
3525              return nil;
3526          }
3527      }
3528  
3529      local from = here();
3530      local riding = location.get(player);
3531      if (riding != nil && riding.isVehicle && !riding.canEnter(to))
3532      {
3533          sayFor('ride.wontgo', riding, nil);"\n";
3534          return nil;
3535      }
3536      if (to == from || player.travelCycle == travelActions.cycle)
3537          return nil;
3538      local escort = accompanists(player, from, target, to);
3539      if (escort == nil)
3540          return nil;
3541      player.travelCycle = travelActions.cycle;
3542      foreach (local a in escort)
3543          a.travelCycle = travelActions.cycle;
3544      foreach (local a in escort)
3545      {
3546          wearTravelState(a, target);
3547          a.sayDeparting(way, target);
3548          relocateActor(a, to);
3549      }
3550  
3551      foreach (local a in escort)
3552      {
3553          local worn = a.curState;
3554          if (worn != nil && worn.ofKind(GuidedTourState)
3555              && worn.stateAfterEscort != nil)
3556              a.setCurState(worn.stateAfterEscort);
3557      }
3558  /*
3559       * Ride what you are in. The player's own container is a containment row
3560       * like any other, so a vehicle travels by moving the thing the player is
3561       * inside and leaving the player inside it.
3562       */
3563      if (riding != nil && riding.isVehicle)
3564          relocateActor(riding, to);
3565      else
3566          relocateActor(player, to);
3567  
3568      if (target.ofKind(TravelConnector) && target.onTravel != nil)
3569          target.onTravel;
3570      describeRoom();
3571      finishAccompaniment(escort, nil);
3572      return nil;
3573  }
3574  
3575  /*
3576   * The actors in this room travelling with this leader, breadth first.
3577   *
3578   * A state says whether it comes along, and a guided tour says so only for the
3579   * leg of the tour it is on — which is what stops a guide following the player
3580   * off the route.
3581   */
3582  accompanists(leader, from, conn, dest)
3583  {
3584      local going = [];
3585      local leaders = [leader];
3586      local candidates = contains.all(from);
3587      local at = 1;
3588      while (at <= leaders.length())
3589      {
3590          local lead = leaders[at];
3591          foreach (local a in candidates)
3592          {
3593              if (!a.isActor || a == leader || a.curState == nil
3594                  || holds(leaders, a))
3595                  continue;
3596              local state = a.curState;
3597              if (state.ofKind(GuidedTourState))
3598              {
3599                  if (state.escortActor != lead
3600                      || (state.escortDest != conn && state.escortDest != dest))
3601                      continue;
3602              }
3603              else if (state.accompanyingActor != lead
3604                       || !state.accompanyTravel(lead, conn))
3605                  continue;
3606              if (leaders.length() >= 64
3607                  || a.travelCycle == travelActions.cycle
3608                  || !a.canAccompanyTravel(lead, conn, dest))
3609                  return nil;
3610              going = going + [a];
3611              leaders = leaders + [a];
3612          }
3613          at = at + 1;
3614      }
3615      return going;
3616  }
3617  
3618  /* A movement fact is independent of prose and visibility. Fiat relocation
3619   * uses this too, but deliberately gathers nobody and consumes no action. */
3620  relocateActor(a, dest)
3621  {
3622      local from = location.get(a);
3623      if (from == dest)
3624          return nil;
3625      contains.set(dest, a);
3626      event('actor.move');
3627      eventSubject(a);
3628      eventSubject(dest);
3629      if (from != nil)
3630          eventSubject(from);
3631      return true;
3632  }
3633  
3634  wearTravelState(a, conn)
3635  {
3636      local state = a.curState;
3637      local lead = state.ofKind(GuidedTourState) ? state.escortActor : state.accompanyingActor;
3638      local worn = state.getAccompanyingTravelState(lead, conn);
3639      if (worn != nil)
3640      {
3641          worn.stateAfter = state;
3642          worn.accompanyingActor = state.accompanyingActor;
3643          worn.escortActor = state.escortActor;
3644          a.setCurState(worn);
3645      }
3646      return nil;
3647  }
3648  
3649  finishAccompaniment(escort, advanceTours)
3650  {
3651      foreach (local a in escort)
3652      {
3653          local worn = a.curState;
3654          if (worn != nil && worn.ofKind(AccompanyingInTravelState))
3655          {
3656              local back = worn.stateAfter;
3657              if (back != nil && back.ofKind(GuidedTourState)
3658                  && back.stateAfterEscort != nil)
3659                  back = back.stateAfterEscort;
3660              a.setCurState(back);
3661          }
3662          else if (advanceTours && worn != nil && worn.ofKind(GuidedTourState)
3663                   && worn.stateAfterEscort != nil)
3664              a.setCurState(worn.stateAfterEscort);
3665      }
3666      return nil;
3667  }
3668  
3669  doBoard(c, item)
3670  {
3671      if (!item.isVehicle)
3672          {
3673              sayFor('board.cannot', item, nil);
3674  "\n";
3675          }
3676      else if (location.get(player) == item)
3677          {
3678              sayFor('board.already', item, nil);
3679  "\n";
3680          }
3681      else
3682      {
3683          contains.set(item, player);
3684          noteDone(c, item);
3685      }
3686      return nil;
3687  }
3688  
3689  doGetOff()
3690  {
3691      local on = location.get(player);
3692      if (on == nil || !(on.isVehicle || on.isEnterable))
3693          {
3694              sayFor('getoff.nothing', nil, nil);
3695  "\n";
3696          }
3697      else
3698      {
3699  /* Out to whatever held it, which may itself be a nested space. */
3700          local out = location.get(on);
3701          contains.set(out == nil ? here() : out, player);
3702          sayFor(on.objPos == posIn ?'getoff.outof' :'getoff.off', on, nil);"\n";
3703      }
3704      return nil;
3705  }
3706  
3707  /* The verb that puts somebody into a posture, for an implied command. */
3708  verbForPosture(want)
3709  {
3710      if (want == sitting)
3711          return 44;
3712      if (want == lying)
3713          return 45;
3714      return 43;
3715  }
3716  
3717  /* What the player is in or on, if anything short of the room. */
3718  nestedIn()
3719  {
3720      local where = location.get(player);
3721      if (where == nil || where.ofKind(Room))
3722          return nil;
3723      return where;
3724  }
3725  
3726  doEnter(c, item)
3727  {
3728      if (item.isVehicle)
3729          return doBoard(c, item);
3730      if (!item.isEnterable)
3731      {
3732          sayFor('enter.cannot', item, nil);"\n";
3733          return nil;
3734      }
3735  
3736      if (item.isLockable && item.isLocked)
3737      {
3738          sayFor('enter.locked', item, nil);"\n";
3739          return nil;
3740      }
3741      if (item.isOpenable && !item.isOpen)
3742      {
3743          sayFor('enter.shut', item, nil);"\n";
3744          return nil;
3745      }
3746      if (item.destination != nil)
3747      {
3748          item.enterDesc;
3749          contains.set(item.destination, player);
3750          describeRoom();
3751          return nil;
3752      }
3753      contains.set(item, player);
3754  /*
3755       * Just read it. An emitting property displays when read and answers nil, so
3756       * testing it for nil would print it and then print the default as well —
3757       * which is why the default lives on the class instead of here.
3758       */
3759      item.enterDesc;
3760  "\n";
3761      return nil;
3762  }
3763  
3764  doPush(item, way)
3765  {
3766      if (!item.canPushTravel)
3767          {
3768              sayFor('push.cannot', item, nil);
3769  "\n";
3770          }
3771      else
3772      {
3773          local target = exits.get(here(), way);
3774          if (target == nil)
3775              {
3776                  sayMsg('go.nowhere', nil);
3777  "\n";
3778              }
3779          else if (target.ofKind(TravelConnector))
3780              {
3781                  sayFor('push.throughconnector', item, target);
3782  "\n";
3783              }
3784          else
3785          {
3786              contains.set(target, item);
3787              contains.set(target, player);
3788              sayFor('push.ok', item, nil);"\n";
3789              describeRoom();
3790          }
3791      }
3792      return nil;
3793  }
3794  
3795  doPutOn(c, item, onto)
3796  {
3797  /* PUT X ON COOKER means the hob, if the cooker says so. */
3798      local named = onto;
3799      if (onto.remapOn != nil)
3800          onto = onto.remapOn;
3801      if (!onto.isContainer)
3802          {
3803              sayFor('puton.cannot', onto, nil);
3804  "\n";
3805          }
3806      else if (item.isFixed)
3807          {
3808              sayFor('put.wontmove', item, nil);
3809  "\n";
3810          }
3811      else if (item == onto)
3812          {
3813              sayMsg('put.initself', nil);
3814  "\n";
3815          }
3816      else if (!roomFor(onto, item))
3817          {
3818              sayFor('puton.noroom', onto, nil);
3819  "\n";
3820          }
3821      else if (onto.notifyInsert(item) != nil)
3822  "<<onto.notifyInsert(item)>>\n";
3823      else
3824      {
3825          contains.set(onto, item);
3826  /* Where it came to rest decides which account is given, so the report
3827           * phase is told rather than left to work it out again. */
3828          c.way_ = onto.objPos == posOn ?'puton.ok' :'putin.ok';
3829          c.iobj = named;
3830          noteDone(c, item);
3831      }
3832      return nil;
3833  }
3834  
3835  /* How much the player has in their arms, for a passage with a limit. */
3836  carriedBulk()
3837  {
3838      local used = 0;
3839      foreach (local item in contains.all(player))
3840      {
3841          if (!item.isWorn)
3842              used = used + item.bulk;
3843      }
3844      return used;
3845  }
3846  
3847  /* How much is already inside something. */
3848  bulkIn(container)
3849  {
3850      local used = 0;
3851      foreach (local item in contains.all(container))
3852          used = used + item.bulk;
3853      return used;
3854  }
3855  
3856  /* Whether one more thing fits. A capacity of 0 means no limit is stated. */
3857  roomFor(container, item)
3858  {
3859  /* A narrow neck refuses a big thing however empty it is. */
3860      if (container.maxSingleBulk != 0 && item.bulk > container.maxSingleBulk)
3861          return nil;
3862      if (container.bulkCapacity == 0)
3863          return true;
3864      return bulkIn(container) + item.bulk <= container.bulkCapacity;
3865  }
3866  
3867  doPutIn(c, item, into)
3868  {
3869  /*
3870       * PUT X IN COOKER means the oven, if the cooker says so. The message names
3871       * what the player named: "in the cooker", not "in the oven space of the
3872       * cooker", which is what leaks if the part answers for itself.
3873       */
3874      local named = into;
3875      if (into.remapIn != nil)
3876          into = into.remapIn;
3877      if (!into.isContainer)
3878          {
3879              sayFor('putin.cannot', into, nil);
3880  "\n";
3881          }
3882      else if (into.isOpenable && !into.isOpen)
3883          {
3884              sayFor('putin.shut', into, nil);
3885  "\n";
3886          }
3887      else if (item.isFixed)
3888          {
3889              sayFor('put.wontmove', item, nil);
3890  "\n";
3891          }
3892      else if (item == into)
3893          {
3894              sayMsg('put.initself', nil);
3895  "\n";
3896          }
3897      else if (!roomFor(into, item))
3898          {
3899              sayFor('putin.noroom', into, nil);
3900  "\n";
3901          }
3902      else if (into.notifyInsert(item) != nil)
3903  "<<into.notifyInsert(item)>>\n";
3904      else
3905      {
3906          contains.set(into, item);
3907          c.way_ = into.objPos == posOn ?'puton.ok'
3908              : into.objPos == posBehind ?'putbehind.ok' :'putin.ok';
3909          c.iobj = named;
3910          noteDone(c, item);
3911      }
3912      return nil;
3913  }
3914  
3915  /* ----------------------------------------------------------------- events */
3916  
3917  /** @api author
3918   * Schedules a property call on an owner after completed turns. */
3919  class Event: object
3920  /* Whose property is called when it fires, and which. */
3921      /** @api author
3922       * Object on which the event's property is called. */
3923      owner = nil
3924      /** @api author
3925       * Property reference called when the event fires. */
3926      prop = nil
3927  /* Turns until it fires. */
3928      /** @api author
3929       * Remaining completed turns before the next firing. */
3930      turnsLeft = 1
3931  /* 0 fires once and stops; n fires every n turns. */
3932      /** @api author
3933       * Number of turns between firings, or zero for a one-shot event. */
3934      interval = 0
3935      /** @api author
3936       * Whether this event participates in scheduling. */
3937      isActive = true
3938  /* Answered nil to skip a firing without stopping the event. */
3939      /** @api author
3940       * Observational condition that can skip a firing without stopping the event. */
3941      isDue = true
3942  ;
3943  
3944  /* Fires once. */
3945  /** @api author
3946   * A one-shot scheduled event. */
3947  class Fuse: Event;
3948  
3949  /* Fires again and again. */
3950  /** @api author
3951   * A scheduled event that repeats at its interval. */
3952  class Daemon: Event
3953      interval = 1
3954  ;
3955  
3956  /** @api author
3957   * A recurring event conditioned on the player's location. */
3958  class SenseEvent: Daemon
3959      where = nil
3960      isDue { return where == nil || where == here(); }
3961  ;
3962  
3963  /** @api author
3964   * An object whose execute hook runs during vhylStart. */
3965  class InitObject: object
3966      execute() { return nil; }
3967  ;
3968  
3969  /* The turn the game is on, counted from the first command. */
3970  turnCount: object value = 0;
3971  
3972  /* Set when the game is over, with what to say. */
3973  gameOver: object done = nil;
3974  
3975  /*
3976   * End the game. The turn that calls this finishes; nothing after it runs.
3977   */
3978  
3979  finishGame(why)
3980  {
3981      gameOver.done = true;
3982  "\n";
3983      if (messageFor(why) != nil)
3984          sayMsg(why, nil);
3985      else
3986  "<<why>>";
3987  "\n";
3988      return nil;
3989  }
3990  
3991  /* Every event that is due, in declaration order. */
3992  
3993  runActors()
3994  {
3995      for (local a = firstObj(Actor); a != nil; a = nextObj(a, Actor))
3996      {
3997          if (a == player)
3998              continue;
3999  
4000          travelActions.backgroundActor = a;
4001          local acted = nil;
4002          for (local item = firstObj(AgendaItem); item != nil;
4003               item = nextObj(item, AgendaItem))
4004          {
4005              if (item.actor != a || !item.isActive)
4006                  continue;
4007  
4008              if (item.isDone)
4009              {
4010                  item.isActive = nil;
4011                  continue;
4012              }
4013              if (!item.isDue)
4014                  continue;
4015              item.turnsLeft = item.turnsLeft - 1;
4016              if (item.turnsLeft > 0)
4017                  continue;
4018              if (item.interval > 0)
4019                  item.turnsLeft = item.interval;
4020              else
4021                  item.isActive = nil;
4022  /* Due, and the actor has already spoken this turn: it has had its
4023               * countdown, and waits its turn rather than losing it. */
4024              if (acted)
4025              {
4026                  item.turnsLeft = 1;
4027                  item.isActive = true;
4028                  continue;
4029              }
4030              if (item.owner != nil && item.prop != nil)
4031              {
4032                  item.owner.(item.prop);
4033                  acted = true;
4034              }
4035              else
4036              {
4037                  item.invokeItem();
4038                  acted = true;
4039              }
4040              if (gameOver.done)
4041              {
4042                  travelActions.backgroundActor = nil;
4043                  return nil;
4044              }
4045          }
4046  
4047          if (!acted)
4048          {
4049              if (a.curState != nil)
4050                  a.curState.takeTurn();
4051              else
4052                  a.idleTurn();
4053          }
4054          a.talkedThisTurn = nil;
4055          travelActions.backgroundActor = nil;
4056      }
4057      return nil;
4058  }
4059  
4060  runEvents()
4061  {
4062      for (local e = firstObj(Event); e != nil; e = nextObj(e, Event))
4063      {
4064          if (!e.isActive)
4065              continue;
4066  
4067          if (e.ofKind(AgendaItem))
4068              continue;
4069          e.turnsLeft = e.turnsLeft - 1;
4070          if (e.turnsLeft > 0)
4071              continue;
4072          if (e.interval > 0)
4073              e.turnsLeft = e.interval;
4074          else
4075              e.isActive = nil;
4076  /* A sense event fires only where it can be noticed. */
4077          if (e.isDue && e.owner != nil && e.prop != nil)
4078              e.owner.(e.prop);
4079          if (gameOver.done)
4080              return nil;
4081      }
4082      return nil;
4083  }
4084  
4085  /* ------------------------------------------------------------------ doers */
4086  
4087  /** @api author
4088   * Redirects or customises a resolved action for selected objects. */
4089  class Doer: object
4090  /* The action this catches, and optionally the object it must be about. */
4091      /** @api author
4092       * Action this Doer intercepts, or nil for any action. */
4093      forAction = nil
4094      /** @api author
4095       * Indirect object this Doer requires, or nil for any object. */
4096      forIobj = nil
4097      /** @api author
4098       * Direct object this Doer requires, or nil for any object. */
4099      forDobj = nil
4100  /* The action to run instead. Answer nil to leave the command alone. */
4101      /** @api author
4102       * Replacement action, or nil to leave the command unchanged. */
4103      instead = nil
4104  /* Whether this Doer applies to the command in hand. */
4105      catches(c)
4106      {
4107          if (forAction != nil && c.action != forAction)
4108              return nil;
4109          if (forIobj != nil && c.iobj != forIobj)
4110              return nil;
4111          if (forDobj != nil && c.dobj != forDobj)
4112              return nil;
4113          return true;
4114      }
4115  ;
4116  
4117  /* The Doer that catches this command, if any. */
4118  findDoer(c)
4119  {
4120      for (local d = firstObj(Doer); d != nil; d = nextObj(d, Doer))
4121      {
4122          if (d.catches(c))
4123              return d;
4124      }
4125      return nil;
4126  }
4127  
4128  /* ---------------------------------------------------------------- parsing */
4129  
4130  grammar command(ordered):
4131      nounPhrase->actorPhrase_',' command->inner_ : Command
4132  ;
4133  
4134  grammar command(look):'look' |'l' : Command action = lookAction;
4135  grammar command(waitCmd):'wait' |'z' : Command action = waitAction;
4136  grammar command(inventory):'inventory' |'i' : Command action = inventoryAction;
4137  grammar command(undoCmd):'undo' : Command action = undoAction;
4138  grammar command(getOff):'get''off' : Command action = getOffAction;
4139  grammar command(standUp):'stand' |'stand''up' |'get''up' : Command action = standAction;
4140  grammar command(sitDown):'sit' |'sit''down' : Command action = sitAction;
4141  grammar command(lieDown):'lie' |'lie''down' : Command action = lieAction;
4142  grammar command(standOn):
4143  'stand' ('on' |'onto') nounPhrase->dobj_ : Command action = standOnAction;
4144  grammar command(sitOn):
4145  'sit' ('on' |'in' |'onto') nounPhrase->dobj_ : Command action = sitOnAction;
4146  grammar command(lieOn):
4147  'lie' ('on' |'in' |'onto') nounPhrase->dobj_ : Command action = lieOnAction;
4148  
4149  grammar command(examine):'examine' nounPhrase->dobj_ : Command action = examineAction;
4150  grammar command(x):'x' nounPhrase->dobj_ : Command action = examineAction;
4151  /* LOOK AT X is what a good many players type, and vhyl had only X and
4152   * EXAMINE. Badness, because LOOK on its own is the room. */
4153  grammar command(lookAt):    [badness 5]'look''at' nounPhrase->dobj_ : Command action = examineAction;
4154  
4155  grammar command(take):      [badness 10]'take' nounPhrase->dobj_ : Command action = takeAction;
4156  grammar command(get):'get' nounPhrase->dobj_ : Command action = takeAction;
4157  grammar command(drop):'drop' nounPhrase->dobj_ : Command action = dropAction;
4158  grammar command(openIt):'open' nounPhrase->dobj_ : Command action = openAction;
4159  grammar command(closeIt):'close' nounPhrase->dobj_ : Command action = closeAction;
4160  grammar command(turnOn):'turn''on' nounPhrase->dobj_ : Command action = turnOnAction;
4161  grammar command(turnIt):'turn' nounPhrase->dobj_ : Command action = turnAction;
4162  grammar command(lightIt):'light' nounPhrase->dobj_ : Command action = turnOnAction;
4163  grammar command(turnOff):'turn''off' nounPhrase->dobj_ : Command action = turnOffAction;
4164  grammar command(board):'board' nounPhrase->dobj_ : Command action = boardAction;
4165  grammar command(rideIt):'ride' nounPhrase->dobj_ : Command action = boardAction;
4166  grammar command(enterIt):'enter' nounPhrase->dobj_ : Command action = enterAction;
4167  grammar command(climbIt):'climb' nounPhrase->dobj_ : Command action = enterAction;
4168  grammar command(wearIt):'wear' nounPhrase->dobj_ : Command action = wearAction;
4169  grammar command(removeIt):'remove' nounPhrase->dobj_ : Command action = doffAction;
4170  grammar command(takeOff):'take''off' nounPhrase->dobj_ : Command action = doffAction;
4171  grammar command(flipIt):'flip' nounPhrase->dobj_ : Command action = flipAction;
4172  grammar command(pullIt):'pull' nounPhrase->dobj_ : Command action = flipAction;
4173  grammar command(pressIt):'press' nounPhrase->dobj_ : Command action = pressAction;
4174  grammar command(pushBtn):'push' nounPhrase->dobj_ : Command action = pressAction;
4175  
4176  grammar command(eatIt):'eat' nounPhrase->dobj_ : Command action = eatAction;
4177  grammar command(drinkIt):'drink' nounPhrase->dobj_ : Command action = drinkAction;
4178  grammar command(pourInto):'pour' nounPhrase->dobj_'into' nounPhrase->iobj_ : Command action = pourIntoAction;
4179  grammar command(pourIn):'pour' nounPhrase->dobj_'in' nounPhrase->iobj_ : Command action = pourIntoAction;
4180  grammar command(detachPart):'detach' nounPhrase->dobj_ : Command action = detachPartAction;
4181  
4182  grammar command(giveTo):'give' nounPhrase->dobj_'to' nounPhrase->iobj_ : Command action = giveAction;
4183  grammar command(showTo):'show' nounPhrase->dobj_'to' nounPhrase->iobj_ : Command action = showAction;
4184  grammar command(unlockWith):'unlock' nounPhrase->dobj_'with' nounPhrase->iobj_ : Command action = unlockAction;
4185  grammar command(unlockIt):'unlock' nounPhrase->dobj_ : Command action = unlockAction;
4186  grammar command(lockWith):'lock' nounPhrase->dobj_'with' nounPhrase->iobj_ : Command action = lockAction;
4187  grammar command(lockIt):'lock' nounPhrase->dobj_ : Command action = lockAction;
4188  
4189  grammar command(sayCmd):
4190  'say' literalPhrase->literal_ : Command action = sayAction;
4191  grammar command(writeOn):
4192  'write' literalPhrase->literal_'on' nounPhrase->dobj_
4193      : Command action = writeOnAction;
4194  grammar command(typeOn):
4195  'type' literalPhrase->literal_'on' nounPhrase->dobj_
4196      : Command action = writeOnAction;
4197  grammar command(thinkAbout):
4198      ('think''about' |'ponder') nounPhrase->dobj_
4199      : Command action = thinkAboutAction;
4200  grammar command(lookUpIn):
4201  'look''up' nounPhrase->iobj_'in' nounPhrase->dobj_
4202      : Command action = consultAction;
4203  grammar command(consultCmd):
4204  'consult' nounPhrase->dobj_'about' nounPhrase->iobj_
4205      : Command action = consultAction;
4206  
4207  grammar command(setTo):'set' nounPhrase->dobj_'to' tokWord->set_ : Command action = setToAction;
4208  grammar command(turnToo):'turn' nounPhrase->dobj_'to' tokWord->set_ : Command action = setToAction;
4209  grammar command(attachTo):'attach' nounPhrase->dobj_'to' nounPhrase->iobj_ : Command action = attachAction;
4210  grammar command(detachFrom):'detach' nounPhrase->dobj_'from' nounPhrase->iobj_ : Command action = detachAction;
4211  grammar command(scoreCmd):'score' : Command action = scoreAction;
4212  grammar command(listenTo):'listen''to' nounPhrase->dobj_ : Command action = listenAction;
4213  grammar command(smellIt):'smell' nounPhrase->dobj_ : Command action = smellAction;
4214  grammar command(askAbout):'ask' nounPhrase->dobj_'about' nounPhrase->iobj_ : Command action = askAboutAction;
4215  grammar command(tellAbout):'tell' nounPhrase->dobj_'about' nounPhrase->iobj_ : Command action = tellAboutAction;
4216  grammar command(putOn):'put' nounPhrase->dobj_'on' nounPhrase->iobj_ : Command action = putOnAction;
4217  grammar command(putIn):'put' nounPhrase->dobj_'in' nounPhrase->iobj_ : Command action = putInAction;
4218  grammar command(putBehind):'put' nounPhrase->dobj_'behind' nounPhrase->iobj_ : Command action = putBehindAction;
4219  grammar command(lookBehind):'look''behind' nounPhrase->dobj_ : Command action = lookBehindAction;
4220  grammar command(lookUnder):'look''under' nounPhrase->dobj_ : Command action = lookUnderAction;
4221  
4222  grammar command(gNorth): ('go'|) ('north' |'n') : Command action = goAction dir_ = north;
4223  grammar command(gSouth): ('go'|) ('south' |'s') : Command action = goAction dir_ = south;
4224  grammar command(gEast):  ('go'|) ('east'  |'e') : Command action = goAction dir_ = east;
4225  grammar command(gWest):  ('go'|) ('west'  |'w') : Command action = goAction dir_ = west;
4226  grammar command(gNE):    ('go'|) ('northeast' |'ne') : Command action = goAction dir_ = northeast;
4227  grammar command(gNW):    ('go'|) ('northwest' |'nw') : Command action = goAction dir_ = northwest;
4228  grammar command(gSE):    ('go'|) ('southeast' |'se') : Command action = goAction dir_ = southeast;
4229  grammar command(gSW):    ('go'|) ('southwest' |'sw') : Command action = goAction dir_ = southwest;
4230  grammar command(gUp):    ('go'|) ('up'    |'u') : Command action = goAction dir_ = up;
4231  grammar command(gDown):  ('go'|) ('down'  |'d') : Command action = goAction dir_ = down;
4232  grammar command(gIn):    ('go'|) ('in' |'inside') : Command action = goAction dir_ = inward;
4233  grammar command(gOut):   ('go'|) ('out' |'outside') : Command action = goAction dir_ = outward;
4234  
4235  grammar command(pushN):'push' nounPhrase->dobj_'north' : Command action = pushDirAction dir_ = north;
4236  grammar command(pushS):'push' nounPhrase->dobj_'south' : Command action = pushDirAction dir_ = south;
4237  grammar command(pushE):'push' nounPhrase->dobj_'east'  : Command action = pushDirAction dir_ = east;
4238  grammar command(pushW):'push' nounPhrase->dobj_'west'  : Command action = pushDirAction dir_ = west;
4239  
4240  class NounPhrase: object
4241      n_ = nil
4242      a1_ = nil
4243      a2_ = nil
4244  
4245      adjs_ = nil
4246      of_ = nil
4247      left_ = nil
4248      trailing_ = nil
4249      also_ = nil
4250  /*
4251       * What an article is in front of. A field of its own rather than `left_`,
4252       * because *the lead and the chart* is an article in front of a **list**,
4253       * and a wrapper that shared a field with the list's left half hid it.
4254       */
4255      under_ = nil
4256  
4257      pronoun_ = nil
4258  ;
4259  
4260  grammar nounPhrase(itOne):
4261      [badness 5]'it' : NounPhrase pronoun_ ='it';
4262  grammar nounPhrase(themOne):
4263      [badness 5]'them' : NounPhrase pronoun_ ='them';
4264  
4265  grammar nounPhrase(himOne):
4266      [badness 5]'him' : NounPhrase pronoun_ ='him';
4267  grammar nounPhrase(herOne):
4268      [badness 5]'her' : NounPhrase pronoun_ ='her';
4269  grammar nounPhrase(allOf):
4270      [badness 5]'all' : NounPhrase pronoun_ ='all';
4271  grammar nounPhrase(everything):
4272      [badness 5]'everything' : NounPhrase pronoun_ ='all';
4273  
4274  /*
4275   * An article, which a player types without thinking about it and every parser
4276   * in the genre accepts. vhyl refused `TAKE THE TOKEN` and `ASK ZOÉ ABOUT THE
4277   * TAPE` until an author game tried to write a walkthrough in English.
4278   *
4279   * A production of its own rather than a word in each rule, so the three noun
4280   * phrase shapes stay three. Spelled `anArticle` because `article` is already
4281   * the language module's a/an chooser, and a production shares a namespace with
4282   * a function.
4283   */
4284  
4285  class AdjWord: object w_ = nil;
4286  
4287  grammar adjWord(fromVocab): vocab.adjective->w_ : AdjWord;
4288  
4289  /* A run of them, so a phrase may carry as many as it likes. */
4290  class AdjRun: object one_ = nil rest_ = nil;
4291  
4292  grammar adjRun(last):  adjWord->one_ : AdjRun;
4293  grammar adjRun(more):  adjWord->one_ adjRun->rest_ : AdjRun;
4294  
4295  grammar anArticle(theOne):'the' : object;
4296  grammar anArticle(aOne):'a' : object;
4297  grammar anArticle(anOne):'an' : object;
4298  
4299  grammar simpleNoun(bare):
4300      vocab.noun->n_ : NounPhrase;
4301  grammar simpleNoun(adjs):
4302      adjRun->adjs_ vocab.noun->n_ : NounPhrase;
4303  
4304  grammar simpleNoun(nounThen):
4305      vocab.noun->n_ adjWord->trailing_ : NounPhrase;
4306  grammar simpleNoun(nounQuoted):
4307      vocab.noun->n_'"' adjWord->trailing_'"' : NounPhrase;
4308  grammar simpleNoun(quotedNoun):
4309  '"' adjWord->trailing_'"' vocab.noun->n_ : NounPhrase;
4310  
4311  /*
4312   * `of` joins two simple names: *piece of paper*, *small pile of dry straw*.
4313   * The thing named is the one both halves name, which is what a game states by
4314   * giving it both words.
4315   */
4316  grammar simpleNoun(ofLink):
4317      simpleNoun->left_'of' simpleNoun->of_ : NounPhrase;
4318  
4319  /* One thing, with or without an article. */
4320  grammar nounPhrase(one):
4321      simpleNoun->under_ : NounPhrase;
4322  grammar nounPhrase(artOne):
4323      anArticle simpleNoun->under_ : NounPhrase;
4324  
4325  grammar nounPhrase(andMore):
4326      simpleNoun->left_ ('and' |',') nounPhrase->also_ : NounPhrase;
4327  grammar nounPhrase(artAndMore):
4328      anArticle simpleNoun->left_ ('and' |',') nounPhrase->also_ : NounPhrase;
4329  
4330  /* ----------------------------------------------------- literals and topics */
4331  
4332  class LiteralPhrase: object
4333      word_ = nil
4334      rest_ = nil
4335  ;
4336  
4337  grammar literalPhrase(oneWord): tokWord->word_ : LiteralPhrase;
4338  grammar literalPhrase(moreWords): tokWord->word_ literalPhrase->rest_ : LiteralPhrase;
4339  grammar literalPhrase(quoted):'"' literalPhrase->rest_'"' : LiteralPhrase;
4340  
4341  /* The words of a literal phrase, joined back into a line. */
4342  literalText(phrase)
4343  {
4344      if (phrase == nil)
4345          return'';
4346  
4347      local head = phrase.word_ == nil || phrase.word_ =='"' ?'' : phrase.word_;
4348      local tail = literalText(phrase.rest_);
4349      if (head =='')
4350          return tail;
4351      return tail =='' ? head : head +' ' + tail;
4352  }
4353  
4354  getLiteral(c)
4355  {
4356      if (c == nil)
4357          return nil;
4358      if (c.literal_ != nil)
4359          return literalText(c.literal_);
4360      return c.set_;
4361  }
4362  
4363  getTopic(c)
4364  {
4365      return c == nil ? nil : c.iobj;
4366  }
4367  
4368  getTopicText(c)
4369  {
4370      if (c == nil)
4371          return nil;
4372      if (c.topicText_ != nil)
4373          return literalText(c.topicText_);
4374      local about = c.iobj;
4375      return about == nil ? nil : about.name;
4376  }
4377  
4378  holds(list, wanted)
4379  {
4380      if (list == nil)
4381          return nil;
4382      return list.indexOf(wanted) != nil;
4383  }
4384  
4385  /* What is in both lists. */
4386  bothOf(left, right)
4387  {
4388      if (left == nil)
4389          return right;
4390      if (right == nil)
4391          return left;
4392      return [for item in left : item if holds(right, item)];
4393  }
4394  
4395  /* The entities a phrase names: every word in it has to name them. */
4396  phraseCandidates(np)
4397  {
4398      if (np == nil)
4399          return [];
4400  
4401      if (np.pronoun_ != nil)
4402      {
4403          if (np.pronoun_ =='all')
4404          {
4405  /*
4406               * Everything to hand — what is in the room and what you are
4407               * carrying. Which of those a verb means is the verb's business:
4408               * TAKE ALL means the room's things and DROP ALL means yours, and
4409               * the ranking says so already, so ALL does not have to.
4410               */
4411              local about = [for item in contains.all(here())
4412                             : item if item != player && !item.isFixed && !item.isDecoration];
4413              foreach (local held in contains.all(player))
4414              {
4415                  if (!held.isFixed && !held.isDecoration)
4416                      about = about + [held];
4417              }
4418              return about;
4419          }
4420          local named = lastNamed.items == nil ? [] : lastNamed.items;
4421  
4422          if (np.pronoun_ =='him' || np.pronoun_ =='her')
4423          {
4424              local wantHim = np.pronoun_ =='him';
4425              local fits = [for x in named : x if wantHim ? x.isHim : x.isHer];
4426              if (fits.length() > 0)
4427                  return fits;
4428              local here_ = contains.all(here()) + contains.all(player);
4429              return [for x in here_ : x if (wantHim ? x.isHim : x.isHer)];
4430          }
4431          return named;
4432      }
4433  /*
4434       * `piece of paper`: the things both halves name. A game says so by giving
4435       * the object both words, which is what its vocabulary is for.
4436       */
4437      if (np.of_ != nil)
4438          return bothOf(phraseCandidates(np.left_), phraseCandidates(np.of_));
4439  /* A wrapper — an article, or the one-thing case — names what it wraps. */
4440      if (np.under_ != nil)
4441          return phraseCandidates(np.under_);
4442  
4443      if (np.also_ != nil)
4444      {
4445          local all = [];
4446          foreach (local part in listedPhrases(np))
4447          {
4448              foreach (local item in phraseCandidates(part))
4449              {
4450                  if (!holds(all, item))
4451                      all = all + [item];
4452              }
4453          }
4454          return all;
4455      }
4456      local list = np.n_;
4457  /* The old pair, kept because a game may still set them directly. */
4458      if (np.a1_ != nil)
4459          list = bothOf(list, np.a1_);
4460      if (np.a2_ != nil)
4461          list = bothOf(list, np.a2_);
4462  /* However many adjectives the phrase carried, in the order written. */
4463      for (local run = np.adjs_; run != nil; run = run.rest_)
4464      {
4465          if (run.one_ != nil)
4466              list = bothOf(list, run.one_.w_);
4467      }
4468      if (np.trailing_ != nil)
4469          list = bothOf(list, np.trailing_.w_);
4470      return list == nil ? [] : list;
4471  }
4472  
4473  lastNamed: object items = nil;
4474  
4475  listedPhrases(np)
4476  {
4477  
4478      if (np == nil)
4479          return [];
4480  /* An article in front of a list is still a list. */
4481      if (np.under_ != nil)
4482          return listedPhrases(np.under_);
4483      if (np.also_ == nil)
4484          return [np];
4485      return listedPhrases(np.left_) + listedPhrases(np.also_);
4486  }
4487  
4488  firstNamed(candidates)
4489  {
4490      if (candidates == nil || candidates.length() == 0)
4491          return nil;
4492      foreach (local c in candidates)
4493      {
4494          if (player.knowsAbout(c))
4495              return c;
4496      }
4497      return candidates[1];
4498  }
4499  
4500  rankOf(item, a, role)
4501  {
4502      local byAction = a.rank(item, role);
4503      local byThing = item.rank(a, role);
4504      return byAction < byThing ? byAction : byThing;
4505  }
4506  
4507  chooseOne(candidates, a, role)
4508  {
4509  
4510      local reachable = [for c in candidates : c if inScope(c) || (c.isDistant && visible(c))];
4511      if (reachable.length() == 0)
4512          return nil;
4513      if (reachable.length() == 1)
4514          return reachable[1];
4515      local best = nil;
4516      foreach (local option in reachable)
4517      {
4518          local score = rankOf(option, a, role);
4519          if (best == nil || score > best)
4520              best = score;
4521      }
4522      local likely = [for c in reachable : c if rankOf(c, a, role) == best];
4523      if (likely.length() == 1)
4524          return likely[1];
4525      sayMsg('parse.which', nil);
4526      foreach (local option in likely)
4527          sayAbout('parse.whichone', option, nil, tellApart(option, likely));
4528  "? ";
4529      local answer = inputLine();
4530      if (answer == nil)
4531          return nil;
4532      foreach (local option in likely)
4533      {
4534          local mark = tellApart(option, likely);
4535          if (option.name == answer
4536              || (mark != nil && (answer == mark
4537                                  || answer == mark +' ' + option.name)))
4538              return option;
4539      }
4540      sayMsg('parse.notone', nil);"\n";
4541      return nil;
4542  }
4543  
4544  tellApart(item, others)
4545  {
4546      for (local st = firstObj(State); st != nil; st = nextObj(st, State))
4547      {
4548          if (st.watches == nil)
4549              continue;
4550          if (st.item != nil && st.item != item)
4551              continue;
4552          if (st.item == nil
4553              && (st.appliesTo == nil || !item.ofKind(st.appliesTo)
4554                  || !st.applies(item)))
4555              continue;
4556          local mine = st.wordNow(item);
4557          if (mine == nil)
4558              continue;
4559  /* Only worth saying when another candidate it would be confused with
4560           * shares the name and differs in this condition. */
4561          foreach (local other in others)
4562          {
4563              if (other == item || other.name != item.name)
4564                  continue;
4565              if (st.wordNow(other) != mine)
4566                  return mine;
4567          }
4568      }
4569      return nil;
4570  }
4571  
4572  readingRank(m)
4573  {
4574      local a = m.action;
4575      if (a == nil)
4576          return rankNever;
4577      if (!a.needsDobj)
4578          return rankLogical;
4579      local best = rankNever;
4580      foreach (local candidate in phraseCandidates(m.dobj_))
4581      {
4582          if (!inScope(candidate) && !candidate.isDistant)
4583              continue;
4584          local score = rankOf(candidate, a, dobjRole);
4585          if (score > best)
4586              best = score;
4587      }
4588      return best;
4589  }
4590  
4591  bestReading(matches)
4592  {
4593      local best = matches[1];
4594      local bestSense = readingRank(best);
4595      local bestBad = best.badness;
4596      foreach (local m in matches)
4597      {
4598          local sense = readingRank(m);
4599          local bad = m.badness;
4600          if (sense > bestSense || (sense == bestSense && bad < bestBad))
4601          {
4602              best = m;
4603              bestSense = sense;
4604              bestBad = bad;
4605          }
4606      }
4607      return best;
4608  }
4609  
4610  endedCommand()
4611  {
4612      sayMsg('game.ended', nil);
4613  "\n";
4614      return nil;
4615  }
4616  
4617  /** @api host
4618   * Submit parser tokens to the library's text command cycle. */
4619  vhylTurn(toks)
4620  {
4621      if (!gameOver.done)
4622          travelActions.cycle = travelActions.cycle + 1;
4623      local m = command.parseTokens(toks, vhylDict);
4624      if (gameOver.done)
4625      {
4626  /* Recognise history navigation without ranking, resolving, asking an
4627           * actor, or running any action hooks in an ended world. */
4628          foreach (local reading in m)
4629          {
4630              if (reading.action == undoAction && reading.actorPhrase_ == nil)
4631                  return undoAction.exec(reading);
4632          }
4633          return endedCommand();
4634      }
4635      if (m.length() == 0)
4636      {
4637          sayMsg('parse.nomatch', nil);
4638  "\n";
4639          return nil;
4640      }
4641      local c = bestReading(m);
4642  /* Undo is history navigation, not another actor/event turn. */
4643      if (c.action == undoAction)
4644          return undoAction.exec(c);
4645  
4646      if (c.actorPhrase_ != nil)
4647      {
4648          local who = firstNamed(phraseCandidates(c.actorPhrase_));
4649          if (who == nil || !who.isActor || !inScope(who))
4650          {
4651              sayMsg('talk.notactor', nil);
4652  "\n";
4653              return nil;
4654          }
4655          local inner = c.inner_;
4656          if (inner == nil || !who.obeyCommand(player, inner.action))
4657          {
4658              sayFor('order.refused', who, nil);
4659  "\n";
4660              return nil;
4661          }
4662          c = inner;
4663          c.actorPhrase_ = nil;
4664      }
4665      local a = c.action;
4666      if (a == nil)
4667      {
4668          sayMsg('parse.nomatch', nil);
4669  "\n";
4670          return nil;
4671      }
4672      if (a.needsDobj)
4673      {
4674  
4675          local listed = listedPhrases(c.dobj_);
4676          if (listed.length() > 1)
4677          {
4678              local named = [];
4679              foreach (local phrase in listed)
4680              {
4681                  local one = a.dobjIsTopic ? firstNamed(phraseCandidates(phrase))
4682                      : chooseOne(phraseCandidates(phrase), a, dobjRole);
4683                  if (one == nil)
4684                  {
4685                      sayMsg(a.missingDobjMsg, c);
4686  "\n";
4687                      return nil;
4688                  }
4689                  named = named + [one];
4690              }
4691              if (a.needsIobj)
4692              {
4693                  c.iobj = a.iobjIsTopic ? firstNamed(phraseCandidates(c.iobj_))
4694                      : chooseOne(phraseCandidates(c.iobj_), a, iobjRole);
4695                  if (c.iobj == nil)
4696                  {
4697                      sayMsg(a.missingIobjMsg, c);
4698  "\n";
4699                      return nil;
4700                  }
4701              }
4702              return runCommandOver(c, named);
4703          }
4704  
4705          local several = c.dobj_ != nil && c.dobj_.pronoun_ =='all';
4706          local candidates = phraseCandidates(c.dobj_);
4707          if (several)
4708          {
4709  
4710              local best = rankNever;
4711              foreach (local item in candidates)
4712              {
4713                  local score = inScope(item) ? rankOf(item, a, dobjRole) : rankNever;
4714                  if (score > best)
4715                      best = score;
4716              }
4717              local wanted = best <= rankNever ? []
4718                  : [for item in candidates
4719                     : item if inScope(item) && rankOf(item, a, dobjRole) == best];
4720              if (wanted.length() == 0)
4721              {
4722                  sayMsg(a.missingDobjMsg, c);
4723  "\n";
4724                  return nil;
4725              }
4726              return runCommandOver(c, wanted);
4727          }
4728          c.dobj = a.dobjIsTopic ? firstNamed(candidates)
4729              : chooseOne(candidates, a, dobjRole);
4730          if (c.dobj == nil)
4731          {
4732              sayMsg(a.missingDobjMsg, c);
4733  "\n";
4734              return nil;
4735          }
4736      }
4737      if (a.needsIobj)
4738      {
4739          c.iobj = a.iobjIsTopic
4740              ? firstNamed(phraseCandidates(c.iobj_))
4741              : chooseOne(phraseCandidates(c.iobj_), a, iobjRole);
4742          if (c.iobj == nil)
4743          {
4744              sayMsg(a.missingIobjMsg, c);
4745  "\n";
4746              return nil;
4747          }
4748      }
4749      return runCommand(c);
4750  }
4751  
4752  runCommand(c)
4753  {
4754      return runCommandOver(c, c.dobj == nil ? nil : [c.dobj]);
4755  }
4756  
4757  runCommandOver(c, items)
4758  {
4759      local a = c.action;
4760      c.done_ = nil;
4761      c.reports_ = nil;
4762      c.failed_ = nil;
4763      if (items == nil)
4764          return finishCommand(c, actOnce(c, a, nil));
4765      foreach (local item in items)
4766      {
4767          c.dobj = item;
4768          actOnce(c, a, item);
4769          if (gameOver.done)
4770              break;
4771      }
4772      return finishCommand(c, a);
4773  }
4774  
4775  /* --------------------------------------------------------- preconditions */
4776  
4777  /** @api author
4778   * Checks or establishes a requirement before an action executes. */
4779  class PreCondition: object
4780      met(item, a, c) { return true; }
4781      impliedVerb(item, a, c) { return nil; }
4782  /* What the implied command acts on, when it is not the thing itself. */
4783      impliedDobj(item, a, c) { return item; }
4784      impliedIobj(item, a, c) { return nil; }
4785      failMsg ='precond.cannot'
4786  ;
4787  
4788  /* The command an implied action runs in, so the outer one is not disturbed. */
4789  impliedArgs: Command;
4790  
4791  tryImplied(verb, item, into, outer)
4792  {
4793      local v = verbFor(verb);
4794      if (v == nil)
4795          return 0;
4796  
4797      if (rankOf(item, v.action, dobjRole) < rankImplicit)
4798          return 0;
4799      local c = impliedArgs;
4800      c.action = v.action;
4801      c.dobj = item;
4802      c.iobj = into;
4803      c.dobj_ = nil;
4804      c.iobj_ = nil;
4805      c.set_ = nil;
4806      c.way_ = v.gerund();
4807      c.list_ = nil;
4808      c.dir_ = v.dir;
4809      c.done_ = nil;
4810      c.reports_ = nil;
4811      c.failed_ = nil;
4812  
4813      c.implied_ = true;
4814  /*
4815       * `sayAbout` rather than `sayFor`, because the gerund is a **word** the
4816       * message needs and `sayFor` carries only entities.
4817       */
4818      local prep = v.preposition();
4819      sayAbout('precond.first', item, into == nil ? nil : [into],
4820               prep == nil ? v.gerund() : v.gerund() +' ' + prep);
4821  "\n";
4822      actOnce(c, c.action, item);
4823      if (c.done_ != nil && c.done_.length() > 0)
4824          c.action.report(c);
4825      sayReports(c);
4826  /* true when it worked, nil when it ran and refused, 0 when it never ran. */
4827      return !c.failed_ && c.done_ != nil && c.done_.length() > 0 ? true : nil;
4828  }
4829  
4830  meetPreConds(c, a, item, role)
4831  {
4832      if (item == nil)
4833          return true;
4834      local conds = a.preCond(item, role) + item.preCond(a, role);
4835      if (conds.length() == 0)
4836          return true;
4837      for (local pass = 0; pass < 8; ++pass)
4838      {
4839          local unmet = nil;
4840          foreach (local p in conds)
4841          {
4842              if (!p.met(item, a, c))
4843              {
4844                  unmet = p;
4845                  break;
4846              }
4847          }
4848          if (unmet == nil)
4849              return true;
4850          local verb = unmet.impliedVerb(item, a, c);
4851          local tried = verb == nil ? 0
4852              : tryImplied(verb, unmet.impliedDobj(item, a, c),
4853                           unmet.impliedIobj(item, a, c), c);
4854          if (tried != true)
4855          {
4856  
4857              if (tried == 0)
4858              {
4859                  sayFor(unmet.failMsg, item, c.iobj);
4860  "\n";
4861              }
4862              return nil;
4863          }
4864      }
4865      sayFor('precond.cannot', item, nil);
4866  "\n";
4867      return nil;
4868  }
4869  
4870  /* Held directly by the player. Implies TAKE. */
4871  objHeld: PreCondition
4872      met(item, a, c) { return carried(item); }
4873      impliedVerb(item, a, c) { return 21; }
4874      failMsg ='precond.held'
4875  ;
4876  
4877  /* Visible. Nothing implies being able to see. */
4878  objVisible: PreCondition
4879      met(item, a, c) { return visible(item); }
4880      failMsg ='precond.visible'
4881  ;
4882  
4883  /* Open, if it is the kind of thing that shuts. Implies OPEN. */
4884  objOpen: PreCondition
4885      met(item, a, c) { return !item.isOpenable || item.isOpen; }
4886      impliedVerb(item, a, c) { return 23; }
4887      failMsg ='precond.open'
4888  ;
4889  
4890  /* Shut. Implies CLOSE. */
4891  objClosed: PreCondition
4892      met(item, a, c) { return !item.isOpenable || !item.isOpen; }
4893      impliedVerb(item, a, c) { return 24; }
4894      failMsg ='precond.closed'
4895  ;
4896  
4897  /* Unlocked. Implies UNLOCK, with no key named — the action finds one. */
4898  objUnlocked: PreCondition
4899      met(item, a, c) { return !item.isLockable || !item.isLocked; }
4900      impliedVerb(item, a, c) { return 41; }
4901      failMsg ='precond.unlocked'
4902  ;
4903  
4904  /* Not being worn. Implies TAKE OFF. */
4905  objNotWorn: PreCondition
4906      met(item, a, c) { return !item.isWorn; }
4907      impliedVerb(item, a, c) { return 33; }
4908      failMsg ='precond.worn'
4909  ;
4910  
4911  /* Nothing inside. Implies taking one thing out, and the loop does the rest. */
4912  objEmpty: PreCondition
4913      met(item, a, c) { return contains.all(item).length() == 0; }
4914      impliedVerb(item, a, c) { return 21; }
4915      impliedDobj(item, a, c)
4916      {
4917          local inside = contains.all(item);
4918          return inside.length() == 0 ? item : inside[1];
4919      }
4920      failMsg ='precond.empty'
4921  ;
4922  
4923  touchObj: PreCondition
4924      met(item, a, c) { return blockingFor(item) == nil; }
4925      impliedVerb(item, a, c) { return 23; }
4926      impliedDobj(item, a, c)
4927      {
4928          local shut = blockingFor(item);
4929          return shut == nil ? item : shut;
4930      }
4931      failMsg ='precond.reach'
4932  ;
4933  
4934  /*
4935   * The nearest shut container between the player and this thing, or nil.
4936   *
4937   * Nearest first, because opening the outer one is what lets you reach the
4938   * inner one, and the loop comes back for the rest.
4939   */
4940  blockingFor(item)
4941  {
4942      local shut = nil;
4943      foreach (local step in contains.ancestors(item))
4944      {
4945  
4946          if (step.isOpenable && !step.isOpen && !step.isTransparent)
4947              shut = step;
4948      }
4949      return shut;
4950  }
4951  
4952  replaceAction(verb, item, other)
4953  {
4954      local v = verbFor(verb);
4955      if (v == nil)
4956          return nil;
4957      local c = impliedArgs;
4958      c.action = v.action;
4959      c.dobj = item;
4960      c.iobj = other;
4961      c.dobj_ = nil;
4962      c.iobj_ = nil;
4963      c.set_ = nil;
4964      c.way_ = nil;
4965      c.list_ = nil;
4966      c.dir_ = v.dir;
4967      c.done_ = nil;
4968      c.reports_ = nil;
4969      c.failed_ = nil;
4970      c.implied_ = nil;
4971      actOnce(c, c.action, item);
4972      if (c.done_ != nil && c.done_.length() > 0)
4973          c.action.report(c);
4974      sayReports(c);
4975      return true;
4976  }
4977  
4978  /* One object's worth of a command: redirect, refuse, act. */
4979  actOnce(c, a, item)
4980  {
4981  
4982      local doer = findDoer(c);
4983      if (doer != nil && doer.instead != nil)
4984      {
4985          a = doer.instead;
4986          c.action = a;
4987      }
4988  
4989      if (a.needsReach)
4990      {
4991  
4992          local far = c.dobj != nil && !a.dobjIsTopic && c.dobj.isDistant ? c.dobj
4993              : c.iobj != nil && !a.iobjIsTopic && c.iobj.isDistant ? c.iobj : nil;
4994          if (far != nil)
4995          {
4996              sayFor('reach.toofar', far, nil);
4997  "\n";
4998              return a;
4999          }
5000  
5001          local high = c.dobj != nil && !a.dobjIsTopic && c.dobj.isOutOfReach
5002              && !c.dobj.canObjReachSelf(player) ? c.dobj
5003              : c.iobj != nil && !a.iobjIsTopic && c.iobj.isOutOfReach
5004              && !c.iobj.canObjReachSelf(player) ? c.iobj : nil;
5005          if (high != nil)
5006          {
5007              sayFor('reach.toohigh', high, nil);
5008  "\n";
5009              return a;
5010          }
5011      }
5012  
5013      if (!meetPreConds(c, a, c.dobj, dobjRole))
5014          return a;
5015      if (!meetPreConds(c, a, a.iobjIsTopic ? nil : c.iobj, iobjRole))
5016          return a;
5017      if (c.dobj != nil)
5018      {
5019          local no = c.dobj.refuses(a, c);
5020          if (no != nil)
5021          {
5022  "<<no>>\n";
5023              return a;
5024          }
5025      }
5026      if (c.iobj != nil)
5027      {
5028          local no = c.iobj.refuses(a, c);
5029          if (no != nil)
5030          {
5031  "<<no>>\n";
5032              return a;
5033          }
5034      }
5035  
5036      local nest = nestedIn();
5037      if (nest != nil && nest.roomBeforeAction(c))
5038          return a;
5039      if (here().roomBeforeAction(c))
5040          return a;
5041  
5042      foreach (local watcher in contains.all(here()))
5043      {
5044          if (watcher.isActor && watcher.curState != nil)
5045              watcher.curState.beforeAction(c);
5046      }
5047      a.exec(c);
5048      foreach (local watcher in contains.all(here()))
5049      {
5050          if (watcher.isActor && watcher.curState != nil)
5051              watcher.curState.afterAction(c);
5052      }
5053      return a;
5054  }
5055  
5056  /*
5057   * The account and the turn, once, however many objects the command named.
5058   */
5059  finishCommand(c, a)
5060  {
5061      if (a == nil)
5062          return nil;
5063  
5064      if (c.done_ != nil && c.done_.length() > 0)
5065      {
5066          a.report(c);
5067  
5068          lastNamed.items = c.done_;
5069      }
5070  
5071      sayReports(c);
5072  
5073      if (!gameOver.done)
5074      {
5075          turnCount.value = turnCount.value + 1;
5076  
5077          runScenes();
5078          runActors();
5079          runEvents();
5080          syncStates();
5081      }
5082      dialogueUI.afterTurn();
5083      return nil;
5084  }
5085  
5086  /*
5087   * The command an action intent fills in. One command runs at a time, so this is
5088   * reused rather than allocated, for the same reason `msgArgs` is.
5089   */
5090  actArgs: Command;
5091  
5092  /* An optional library module may handle structured UI intents. */
5093  dialogueUI: object
5094      accepts(verb) { return nil; }
5095      act(verb, args) { return nil; }
5096      afterTurn() { return nil; }
5097  ;
5098  
5099  /** @api host
5100   * Submit a structured verb code and entity arguments to the normal action cycle. */
5101  vhylAct(verb, subjects)
5102  {
5103      if (dialogueUI.accepts(verb))
5104          return dialogueUI.act(verb, subjects);
5105      if (!gameOver.done)
5106          travelActions.cycle = travelActions.cycle + 1;
5107      local v = verbFor(verb);
5108      if (gameOver.done)
5109      {
5110          if (v != nil && v.action == undoAction)
5111              return undoAction.exec(nil);
5112          return endedCommand();
5113      }
5114      if (v == nil)
5115      {
5116          sayAbout('act.noverb', nil, nil, verb);
5117  "\n";
5118          return nil;
5119      }
5120      local c = actArgs;
5121      local a = v.action;
5122      if (a == undoAction)
5123          return undoAction.exec(nil);
5124      c.action = a;
5125      c.dobj_ = nil;
5126      c.iobj_ = nil;
5127      c.dobj = nil;
5128      c.iobj = nil;
5129      c.way_ = nil;
5130      c.list_ = nil;
5131      c.dir_ = v.dir;
5132      local count = subjects == nil ? 0 : subjects.length();
5133      if (a.needsDobj)
5134      {
5135          if (count < 1)
5136          {
5137              sayMsg(a.missingDobjMsg, c);
5138  "\n";
5139              return nil;
5140          }
5141          c.dobj = subjects[1];
5142      }
5143      if (a.needsIobj)
5144      {
5145          if (count < 2)
5146          {
5147              sayMsg(a.missingIobjMsg, c);
5148  "\n";
5149              return nil;
5150          }
5151          c.iobj = subjects[2];
5152      }
5153  /*
5154       * Scope is a world rule, so it is checked here too. A topic is deliberately
5155       * exempt: you can ask about a wreck on the shore below without it being to
5156       * hand, which is the same exemption the parser makes.
5157       */
5158      if (c.dobj != nil && !inScope(c.dobj))
5159      {
5160          sayFor('act.outofreach', c.dobj, nil);
5161  "\n";
5162          return nil;
5163      }
5164      if (c.iobj != nil && !a.iobjIsTopic && !inScope(c.iobj))
5165      {
5166          sayFor('act.outofreach', c.iobj, nil);
5167  "\n";
5168          return nil;
5169      }
5170      return runCommand(c);
5171  }
5172  
5173  recover(code)
5174  {
5175      sayMsg('turn.failed', nil);
5176  "\n> ";
5177      return nil;
5178  }