DM4 §44: Case study: a library file for menus

## §44 Case study: a library file for menus

> Yes, all right, I won't do the menu … I don't think you realise how long it takes to do the menu, but no, it doesn't matter, I'll hang the picture now. If the menus are late for lunch it doesn't matter, the guests can all come and look at the picture till they are ready, right?
> — John Cleese and Connie Booth, Fawlty Towers

Sometimes one would like to provide a menu of text options, offered to the player as a list on screen which can be rummaged through with the cursor keys. For instance, the hints display in the “solid gold” edition of Infocom's ‘Zork I’ shows a list of “Invisiclues”: “Above Ground”, “The Cellar Area”, and so on. Moving a cursor to one of these options and pressing RETURN brings up a sub-menu of questions on the general topic chosen: for instance, “How do I cross the mountains?” Besides hints, many modern games use menu displays for instructions, background information, credits and release notes.

An optional library file called `"Menus.h"` is provided to manage such menus. If you want its facilities then, where you previously included `Verblib`, now write:

    Include "Verblib";
    Include "Menus";

And this will make the features of `Menus.h` available. This section describes what these simple features are, and how they work, as an extended example of Z-machine programming.

The designer of this system began by noticing that menus and submenus and options fit together in a tree structure rather like the object tree:

Hints for ‘Zork I’ (menu)
→ Above Ground (submenu)
→ How do I cross the mountains? (option)
→ some text is revealed
→ The Cellar Area (submenu)
→ ...

The library file therefore defines two classes of object, `Menu` and `Option`. The short name of a menu is its title, while its children are the possible choices, which can be of either class. (So you can have as many levels of submenu as needed.) Since choosing an `Option` is supposed to produce some text, which is vaguely like examining objects, the `description` property of an `Option` holds the information revealed. So, for instance:

    Menu hints_menu "Hints for Zork I";
    Menu -> "Above Ground";
    Option -> -> "How do I cross the mountains?"
      with description "By ...";
    Menu -> "The Cellar Area";

Note that such a structure can be rearranged in play just as the rest of the object tree can, which is convenient for “adaptive hints”, where the hints offered vary with the player's present travail.

How does this work? A menu or an option is chosen by being sent the message `select`. So the designer will launch the menu, perhaps in response to the player having typed “hints”, like so:

    [ HintsSub;
      hints_menu.select();
    ];

As the player browses through the menu, each menu sends the `select` message to the next one chosen, and so on. This already suggests that menus and options are basically similar, and in fact that's right: `Menu` is actually a subclass of `Option`, which is the more basic idea of the two.

· · · · ·

The actual code of `Menus.h` is slightly different from that given below, but only to fuss with dealing with early copies of the rest of the library, and to handle multiple languages. It begins with the class definition of `Option`, as follows:

    Class Option
     with select [;
              self.emblazon(1, 1, 1);
              @set_window 0; font on; style roman; new_line; new_line;
              if (self provides description) return self.description();
              "[No text written for this option.]^";
          ],

The option sends itself the message `emblazon(1,1,1)` to clear the screen an put a bar of height 1 line at the top, containing the title of the option centred. The other two 1s declare that this is “page 1 of 1”: see below. Window 0 (the ordinary, lower window) is then selected; text reverts to its usual state of being roman-style and using a variable-pitched font. The screen is now empty and ready for use, and the option expects to have a `description` property which actually does any printing that's required. To get back to the emblazoning:

    emblazon [ bar_height page pages temp;
        screen_width = 0->33;
        !   Clear screen:
        @erase_window -1;
        @split_window bar_height;
        !   Black out top line in reverse video:
        @set_window 1;
        @set_cursor 1 1;
        style reverse; spaces(screen_width);
        if (standard_interpreter == 0)
            @set_cursor 1 1;
        else {
            ForUseByOptions-->0 = 128;
            @output_stream 3 ForUseByOptions;
            print (name) self;
            if (pages ~= 1) print " [", page, "/", pages, "]";
            @output_stream -3;
            temp = (screen_width - ForUseByOptions-->0)/2;
            @set_cursor 1 temp;
        }
        print (name) self;
        if (pages ~= 1) print " [", page, "/", pages, "]";
        return ForUseByOptions-->0;
    ];

That completes `Option`. However, since this code refers to a variable and an array, we had better write definitions of them:

    Global screen_width;
    Global screen_height;
    Array ForUseByOptions -> 129;

(The other global variable, `screen_height`, will be used later. The variables are global because they will be needed by all of the menu objects.) The `emblazon` code checks to see if it's running on a standard interpreter. If so, it uses output stream 3 into an array to measure the length of text like “The Cellars [2/3]” in order to centre it on the top line. If not, the text appears at the top left instead.

So much for `Option`. The definition of `Menu` is, inevitably, longer. It inherits `emblazon` from its superclass `Option`, but overrides the definition of `select` with something more elaborate:

    Class Menu class Option
     with select [ count j obj pkey line oldline top_line bottom_line
            page pages options top_option;
              screen_width = 0->33;
              screen_height = 0->32;
              if (screen_height == 0 or 255) screen_height = 18;
              screen_height = screen_height - 7;

The first task is to work out how much room the screen has to display options. The width and height, in characters, are read out of the story file's header area, where the interpreter has written them. In case the interpreter is really poor, we guess at 18 if the height is claimed to be zero or 255; since this is a library file and will be widely used, it errs on the side of extreme caution. Finally, 7 is subtracted because seven of the screen lines are occupied by the panel at the top and white space above and below the choices. The upshot is that `screen_height` is the actual maximum number of options to be offered per page of the menu. Next: how many options are available?

              options = 0;
              objectloop (obj in self && obj ofclass Option) options++;
              if (options == 0) return 2;

(Note that a `Menu` is also an `Option`.) We can now work out how many pages will be needed.

              pages = options/screen_height;
              if (options%screen_height ~= 0) pages++;
              top_line = 6;
              page = 1;
              line = top_line;

`top_line` is the highest screen line used to display an option: line 6. The local variables `page` and `line` show which line on which page the current selection arrow points to, so we're starting at the top line of page 1.

              .ReDisplay;
              top_option = (page - 1) * screen_height;

This is the option number currently selected, counting from zero. We display the three-line black strip at the top of the screen, using `emblazon` to create the upper window:

              self.emblazon(7 + count, page, pages);
              @set_cursor 2 1; spaces(screen_width);
              @set_cursor 2 2; print "N = next subject";
              j = screen_width-12; @set_cursor 2 j; print "P = previous";
              @set_cursor 3 1; spaces(screen_width);
              @set_cursor 3 2; print "RETURN = read subject";
              j = screen_width-17; @set_cursor 3 j;

The last part of the black strip to print is the one offering Q to quit:

              if (sender ofclass Option) print "Q = previous menu";
              else print "  Q = resume game";
              style roman;

The point of this is that pressing Q only takes us back to the previous menu if we're inside the hierarchy, i.e., if the message `select` was sent to this `Menu` by another `Option`; whereas if not, Q takes us out of the menu altogether. Next, we count through those options appearing on the current page and print their names.

              count = top_line; j = 0;
              objectloop (obj in self && obj ofclass Option) {
                  if (j >= top_option && j < (top_option+screen_height)) {
                      @set_cursor count 6;
                      print (name) obj;
                      count++;
                  }
                  j++;
              }
              bottom_line = count - 1;

Note that the name of the option begins on column 6 of each line. The player's current selection is shown with a cursor `>` appearing in column 4:

              oldline = 0;
              for (::) {
                  ! Move or create the > cursor:
                  if (line ~= oldline) {
                      if (oldline ~= 0) {
                          @set_cursor oldline 4; print " ";
                      }
                      @set_cursor line 4; print ">";
                  }
                  oldline = line;

Now we wait for a single key-press from the player:

                  @read_char 1 -> pkey;
                  if (pkey == 'N' or 'n' or 130) {
                      ! Cursor down:
                      line++;
                      if (line > bottom_line) {
                          line = top_line;
                          if (pages > 1) {
                              if (page == pages) page = 1; else page++;
                              jump ReDisplay;
                          }
                      }
                      continue;
                  }

130 is the ZSCII code for “cursor down key”. Note that if the player tries to move the cursor off the bottom of the list, and there's at least one more page, we jump right out of the loop and back to `ReDisplay` to start again from the top of the next page. Handling the “previous” option is very similar, and then:

                  if (pkey == 'Q' or 'q' or 27 or 131) break;

Thus pressing lower or upper case Q, escape (ZSCII 27) or cursor left (ZSCII 131) all have the same effect: to break out of the for loop. Otherwise, one can press RETURN or cursor right to select an option:

                  if (pkey == 10 or 13 or 132) {
                      count = 0;
                      objectloop (obj in self && obj ofclass Option) {
                          if (count == top_option + line - top_line) break;
                          count++;
                      }
                      switch (obj.select()) {
                          2: jump ReDisplay;
                          3: jump ExitMenu;
                      }
                      print "[Please press SPACE to continue.]^";
                      @read_char 1 -> pkey;
                      jump ReDisplay;
                  }
              }

(No modern interpreter should ever give 10 for the key-code of RETURN, which is ZSCII 13. Once again, the library file is erring on the side of extreme caution.) An option's `select` routine can return three different values for different effects:

2 | Redisplay the menu page that selected me
3 | Exit from that menu page
anything else | Wait for SPACE, then redisplay that menu page

Finally, the exit from the menu, either because the player typed Q, escape, etc., or because the selected option returned 3:

              .ExitMenu;
              if (sender ofclass Option) return 2;
              font on; @set_cursor 1 1;
              @erase_window -1; @set_window 0;
              new_line; new_line; new_line;
              if (deadflag == 0) <<Look>>;
              return 2;
          ];

And that's it. If this menu was the highest-level one, it needs to resume the game politely, by clearing the screen and performing a `Look` action. If not, then it needs only to return 2, indicating “redisplay the menu page that selected me”: that is, the menu one level above.

The only remaining code in `"Menus.h"` shows some of the flexibility of the above design, by defining a special type of option:

    Class SwitchOption class Option
      with short_name [;
               print (object) self, " ";
               if (self has on) print "(on)"; else print "(off)";
               rtrue;
           ],
           select [;
               if (self has on) give self ~on; else give self on;
               return 2;
           ];

Here is an example of `SwitchOptions` in use:

    Menu settings "Game settings";
    SwitchOption -> FullRoomD   "full room descriptions" has on;
    SwitchOption -> WordyP      "wordier prompts";
    SwitchOption -> AllowSavedG "allow saved games" has on;

So each option has the attribute `on` only if currently set. In the menu, the option `FullRoomD` is displayed either as “full room descriptions (on)” or “full room descriptions (off)”, and selecting it switches the state, like a light switch. The rest of the code can then perform tests like so:

    if (AllowSavedG hasnt on) "That spell is forbidden.";

· · · · ·

Appearance of the final menu on a screen 64 characters wide:
line 1 | `Hints for Zork I [1/2]`
line 2 | `N = next subject P = previous`
line 3 | `RETURN = read subject Q = resume game`
line 4 | ``
line 5 | ``
line 6 | `Above Ground`
line 7 | `> The Cellar Area`
line 8 | `The Maze`
line 9 | `The Round Room Area`

• REFERENCES
Because there was a crying need for good menus in the early days of Inform, there are now numerous library extensions to support menus and interfaces built from them. The original such was L. Ross Raszewski's `"domenu.h"`, which provides a core of basic routines. `"AltMenu.h"` then uses these routines to emulate the same menu structures coded up in this section. `"Hints.h"` employs them for Invisiclues-style hints; `"manual.h"` for browsing books and manuals; `"converse.h"` for menu-based conversations with people, similar to those in graphical adventure games. Or indeed to those in Adam Cadre's game ‘Photopia’, and Adam has kindly extracted his menu-based conversational routines into an example program called `"phototalk.inf"`. For branching menus, such as a tree of questions and answers, try Chris Klimas's `"branch.h"`. To put a menu of commands at the status line of a typical game, try Adam Stark's `"action.h"`.