Exercise 19 - Locks and Gadgets (adv3)

This game is a demonstration of Locks, Keys, Gadgets and Controls.

This is a complete game insofar as there is an objective and it's possible to win, but it's not really complete in terms of implementing everything that ought to be implemented in a real game. It's basically a coding demonstration.

The main purpose of this demo is to illustrate the use of the various Lockable, Key, gadget and control-type classes. It has, of course, been necessary to include some objects from various other class-families as well in order to make a coherent game, but in the main these have been kept to a minimum.

The game follows the specification of Exercise 19 in Learning TADS 3:

The player character is outside the home of a blackmailer. Knowing him to be out, the player wants to burgle his house to recover an incriminating letter. The player character carries a small black case holding a skeleton key and a key-ring, and also has a small flashlight (if you want to be really sophisticated you can try to see whether the player refers to it as a ‘flashlight’ or a ‘torch’ and then use American or British English from then on accordingly). The front door can be unlocked either with the skeleton key or with the key hidden under a nearby flowerpot. Once inside the hall the player character must disable the burglar alarm before going any further into the house. The alarm is controlled by a numeric keypad inside a box by the front door. The correct combination is the date (year) that was written over the outside of the front door. To unlock the box containing the keypad requires either the skeleton key or a small silver key that falls to the ground when the player character pulls a peg on the nearby hat-stand. Once the alarm has been turned off, the player character can go into the study. On one wall of the study is a panel than needs to be opened to gain access to the safe. In the study is a desk on which is a small wooden box. On the side of the box is a slider, which can be set to the names of four different composers; the box is unlocked when the slider is used to spell out the word OPEN from the initial letters of these composers. Inside the box is a key that can be unused to unlock the drawer of the desk. This contains a notebook in which is written the cryptic message “Advertising is safe” together with the combination of the safe. There’s a TV in the study which can be turned on and off with a switch, and changed to different channels with a dial. Turning it on and switching it to the advertising channel will open the panel in the wall. The player character can then go through the open panel into a small cubby-hole containing the safe. From inside the cubby-hole the panel can be opened and closed by means of a lever. The safe has dial which must be turned to each of the numbers in the combination for the safe to be unlocked. Once the safe is unlocked it can be opened and the letter retrieved. The game is won when the player character walks away from the house carrying the letter.

#charset "us-ascii"

/*   
 *   LOCKS AND GADGETS
 *
 *   A demonstration of Locks, Keys, Gadgets and Controls.
 *
 *   This is a complete game insofar as there is an objective and it's 
 *   possible to win, but it's not really complete in terms of implementing 
 *   everything that ought to be implemented in a real game. It's basically 
 *   a coding demonstration.
 *
 *   The main purpose of this demo is to illustrate the use of the various 
 *   Lockable, Key, gadget and control-type classes. It has, of course, been 
 *   necessary to include some objects from various other class-families as 
 *   well in order to make a coherent game, but in the main these have been 
 *   kept to a minimum.
 */

/* 
 *   Include the main header for the standard TADS 3 adventure library.
 *   Note that this does NOT include the entire source code for the
 *   library; this merely includes some definitions for our use here.  The
 *   main library must be "linked" into the finished program by including
 *   the file "adv3.tl" in the list of modules specified when compiling.
 *   In TADS Workbench, simply include adv3.tl in the "Source Files"
 *   section of the project.
 *   
 *   Also include the US English definitions, since this game is written
 *   in English.  
 */
#include <adv3.h>
#include <en_us.h>

/*
 *   VERSION INFO
 *
 *   Our game credits and version information.  This object isn't required 
 *   by the system, but our GameInfo initialization above needs this for 
 *   some of its information.
 *
 *   You'll have to customize some of the text below, as marked: the name of 
 *   your game, your byline, and so on.
 */
versionInfo: GameID
    IFID = '77eebefc-9f19-bc82-2530-65e817e6b811'
    name = 'Locks and Gadgets'
    byline = 'by Eric Eve'
    htmlByline = 'by <a href="mailto:eric.eve@hmc.ox.ac.uk">
                  Eric Eve</a>'
    version = '0.4'
    authorEmail = 'Eric Eve <eric.eve@hmc.ox.ac.uk>'
    desc = 'A demo of TADS 3 Locks, Keys, Gadgets and Controls'
    htmlDesc = 'A demo of TADS 3 Locks, Keys, Gadgets and Controls'
;

/*
 *   GAME MAIN
 *
 *   The "gameMain" object lets us set the initial player character and 
 *   control the game's startup procedure.  Every game must define this 
 *   object.  For convenience, we inherit from the library's GameMainDef 
 *   class, which defines suitable defaults for most of this object's 
 *   required methods and properties.  
 */
gameMain: GameMainDef
    /* the initial player character is 'me' */
    initialPlayerChar = me
    showIntro()
    {
        "Baron Lothar von Erpresser is out; you know because you arranged for
        him to be sent an invitation to the reception at the Patagonian
        embassy, which he was unable to resist. His house will be empty for the
        next few hours, affording you the best opportunity to recover that
        unfortunate letter. You're not prepared to pay the price he's asking
        for it, and if he carries out his threat to send it to your betrothed
        your marriage prospects will be seriously compromised.\b
        You're pretty sure he's keeping the incriminating letter in a safe in
        his study. You're well equipped to recover it; all you need to do now
        is enter the house, recover the letter, and make a quick getaway. The
        baron has such a poor memory for numbers that you feel sure he'll have
        written down the combination somewhere. \b";
    }
    
;

/*  
 *   ENUMERATOR
 *
 *   We can define an enumerator with whatever names we like. Here we'll 
 *   define a couple of values to keep track of the nationality of the player
 *   (character).
 */

enum british, american;


/* 
 *   OUTDOOR ROOM
 *
 *   Starting location - we'll use this as the player character's initial 
 *   location.  The name of the starting location isn't important to the 
 *   library, but note that it has to match up with the initial location for 
 *   the player character, defined in the "me" object below.
 *
 *   Our definition defines two strings.  The first string, which must be in 
 *   single quotes, is the "name" of the room; the name is displayed on the 
 *   status line and each time the player enters the room.  The second 
 *   string, which must be in double quotes, is the "description" of the 
 *   room, which is a full description of the room.  This is displayed when 
 *   the player types "look around," when the player first enters the room, 
 *   and any time the player enters the room when playing in VERBOSE mode.
 *
 */
drive: OutdoorRoom 'Front Drive'
    "Langtree House, a sprawling Edwardian mansion, lies before you just to the
    south, while to the north the drive leads back down to the road. "
    south = frontDoorOutside
    in asExit(south)
    
    north: TravelConnector {        
        
        /* 
         *   We prevent the player character from leaving towards the road 
         *   until s/he is carrying the letter. When the player character 
         *   does leave for the road, we'll end the game.
         */
        canTravelerPass(traveler) { return letter.isIn(traveler); }
        explainTravelBarrier(traveler)
        {
            "You're <i>not</i> leaving without that letter! ";            
        }
        
        /*  
         *   Once the PC travels via this connector, the game is won, so we 
         *   end the game in victory (i.e. display a YOU HAVE WON message 
         *   and end the game.
         */
        
        noteTraversal(traveler)
        {
            "As you walk down the drive you tuck the letter safely inside your
            coat. The baron won't be back for hours yet; by the time he finds
            the letter has gone it'll have been burned to cinders in your
            hearth. And what can he do then? Report the theft to the police?\b
            You chuckle merrily at the thought as you walk out into the
            road.\b";
            finishGameMsg(ftVictory, [finishOptionUndo, finishOptionAmusing]);
        }
    }
;

/*
 *   ACTOR - PLAYER CHARACTER
 *
 *   Define the player character.  The name of this object is not important, 
 *   but it MUST match the name we use to initialize 
 *   gameMain.initialPlayerChar above.
 *
 *   Note that we aren't required to define any vocabulary or description 
 *   for this object, because the class Actor, defined in the library, 
 *   automatically provides the appropriate definitions for an Actor when 
 *   the Actor is serving as the player character.  Note also that we don't 
 *   have to do anything special in this object definition to make the Actor 
 *   the player character; any Actor can serve as the player character, and 
 *   we'll establish this one as the PC in main(), below.  
 */
+ me: Actor
    desc = "You are dressed all in black, as befits a burglar. "
    
    /*  
     *   A custom property that will be used later on. The possible values 
     *   are british and american.
     */
    nationality = british
;

/*  
 *   FLASHLIGHT
 *
 *   A Flashlight is both a gadget and a LightSource, being a combination of 
 *   LightSource and Switch.  
 *
 *   The ostensible purpose of this torch/flashlight is to allow the player 
 *   character to see in the darkened hall, and it is indeed the kind of 
 *   thing one might expect some intended burglary to be carrying. The real 
 *   purpose is to guess when the player is more comfortable with British or 
 *   American English. A British player will most likely call this device a 
 *   'grey torch' while an American will be more likely to refer to it is a 
 *   'gray flashlight'.
 */

++ torch: Flashlight 'plastic grey gray switch/torch/flashlight/tube' 
    'plastic tube'
    "It's a <<name>>. "
    
    /* 
     *   The initDesc is used in place of the ordinary desc property while 
     *   the object isInInitState is true. Here we defined isInInitState to 
     *   be true so long as this object is called 'plastic tube'. Once this 
     *   changes we can change from this oblique functional description to a 
     *   briefer, more conventional one.
     */
    initDesc =  "It's a plastic tube of a colour midway between black and
        white, with a switch than can be turned on and off to produce light. "
    isInInitState = (name == 'plastic tube')
    
    /*  
     *   The normal purpose of matchNameCommon() is to decide whether we 
     *   want to interfere with the parser's choice of this object as match 
     *   for what the player typed. We don't interfere with that here at 
     *   all; instead we take advantage of the fact that this routine is 
     *   called whenever the player refers to this object to see what words 
     *   the player used to refer to it.
     */
    
    matchNameCommon(origTokens, adjustedTokens)
    {
        /* 
         *   Don't worry too much if the next statement looks like a piece of
         *   arcane mumbo-jumbo. The adjustedTokens parameter will contain a
         *   list that looks something like ['grey', &adjective, 'torch', 
         *   &noun]. What the following statement does is to ensure that all 
         *   the string values in the list are converted to lower case while 
         *   leaving the others untouched. This makes it easier to see 
         *   whether words like 'torch' or 'flashlight' occur in the list 
         *   without worrying whether the player typed them in upper or 
         *   lower case.
         */
        
        local lst = adjustedTokens.mapAll({x: dataType(x) == 
                                          TypeSString ? x.toLower() : x});
        
        /* 
         *   Test first to see if the player has used the word 'torch' or 
         *   'flashlight', and if so use that to determine the name of this 
         *   object (and the nationality of the player). Otherwise see if 
         *   'grey' or 'gray' has been used.
         */
        
        if(lst.indexOf('torch'))        
            name = britishName;
        else if (lst.indexOf('flashlight'))
            name = americanName;
        else if(lst.indexOf('grey'))
            name = britishName;
        else if(lst.indexOf('gray'))
            name = americanName;
               
        /* 
         *   Now set the nationality of the player according to the name of 
         *   this object. Note that if the player refers to us a plastic 
         *   tube we can't make a decision on nationality and so we won't 
         *   change it.
         */
        
        if(name == britishName)
            me.nationality = british;
        if(name == americanName)
            me.nationality = american;
            
        return inherited(origTokens, adjustedTokens);
    }
    britishName = 'grey torch'
    americanName = 'gray flashlight'
;


/*  
 *   LOCKABLE CONTAINER
 *
 *   This is a basic LockableContainer; it has a lock but no key is needed to
 *   unlock it, and opening the case will perform an implicit unlock 
 *   action, so the lock performs virtually no function in practice. We'll 
 *   meet some more challenging lockable containers below.
 */

++ LockableContainer 'small black case' 'small black case'
    "It's very small, not big enough to impede your movements, just large
    enough to contain some essential equipment for the job. "
    
    /* 
     *   We've described the case as very small, so let's make its 
     *   bulkCapacity match that.
     */
    bulkCapacity = 3
    bulk = 3
;

/*   
 *   KEY
 *
 *   As its name implies, this skeleton key will be able to open just about 
 *   any keyed lock in the game. This will be used to illustrate that one 
 *   key can open several locks, and that several keys can be defined as 
 *   opening the same lock.
 */

+++ skeletonKey: Key 'thin skeleton metal key*keys' 'skeleton key'
    "It's a thin metal key, with cunningly designed teeth. "
;

/*
 *   KEYRING
 *
 *   The Keyring class is a nice convenience feature for the player. Once 
 *   the PC is carrying the keyring, any key picked up will be added to the 
 *   keyring; moreover, when an attempt is made to unlock a previously 
 *   unencountered door or container, the game will automatically try every 
 *   key on the ring until it finds one that fits (or that none of them do).
 */ 

+++ Keyring 'gold keyring/ring' 'gold keyring'    
;

/*  
 *   ENTERABLE
 *
 *   We use an Enterable to represent the house and point its connector 
 *   property to the front door (so that ENTER HOUSE will attempt to take 
 *   the PC through the front door.
 */

+ Enterable -> frontDoorOutside 'langtree edwardian sprawling house/mansion' 
    'house'
    "It looks a little gloomy and intimidating in the twilight, but most of all
    you resent its occupancy by a man who makes his money in so vile a fashion.
    "
;




/*
 *   LOCKABLE WITH KEY, DOOR
 *
 *   A door is an obvious thing to lock and unlock with a key, and here we 
 *   provide a simple example. The description provides a hint for a 
 *   combination to be used just inside. Since the door describes itself as 
 *   hard to break, we provide a corresponding custom shouldNotBreakMsg to 
 *   respond to an attempt to break the door.
 */

+ frontDoorOutside: LockableWithKey, Door -> frontDoorInside
    'solid oak front door/lintel' 'front door'
    "The date carved on the lintel, 1908, confirms that the house is indeed
    Edwardian. The door itself is of solid oak; there's no way you're going to
    break it down. "
    shouldNotBreakMsg = 'It\'s made of solid oak; there\'s no way you can break
        it down. '
    
    /* There are two different keys that can unlock this door. */
    keyList = [doorKey, skeletonKey]
;

/*   
 *   UNDERSIDE
 *
 *   Hiding a key under a pot just by the door hardly constitutes an exciting
 *   puzzle, but here the point is simply to provide an example of two keys 
 *   opening the same door.
 */

+ pot: Underside 'old flower pot/flowerpot' 'flowerpot'
    initSpecialDesc = "An old flowerpot rests on the ground near the front door.
        "
;

/*  
 *   KEY
 *
 *   This is the other key that will unlock the front door. 
 */

++ doorKey: Hidden, Key 'small brass key*keys' 'small brass key'
;


//------------------------------------------------------------------------------
/*  
 *   TRAVEL BARRIER
 *
 *   We define this TravelBarrier object here so that we can go on to use it 
 *   on three different connectors inside the house. The idea is to prevent 
 *   the PC from going beyond the hall until s/he's turned off the burglar 
 *   alarm.
 */

alarmBarrier: TravelBarrier
    canTravelerPass(traveler) { return !alarmPanel.isOn; }
    explainTravelBarrier(traveler)
    {
        "You daren't go further into the house until you've disabled the
        alarm. ";
    }
;

/*  DARK ROOM */

hall: DarkRoom 'Hall'
    "The hall reflects a kind of fading grandeur, as if struggling to recall
    the happier days of imperial spendour in which it was built, long before it
    fell into the hands of a dastardly blackmailing foreigner. The way to the
    dastard's study is immediately to the east. The gloomy hall continues
    southwards towards the kitchen while a broad flight of stairs leads up to the
    floor above. The front door lies to the north, with an incongruously modern
    white box set on the wall just by it. "
    
    north = frontDoorInside
    out asExit(north)
    
    /* 
     *   This ONE WAY ROOM CONNECTOR is in place simply so we can put the 
     *   alarm barrier on it. The player can't go east from the hall until 
     *   the alarm has been switched off.
     */
    
    east: OneWayRoomConnector
    {
        -> study
        travelBarrier = [alarmBarrier]
    }
    
    /*   
     *   This FAKE CONNECTOR exists simply to make the house appear bigger 
     *   than the number of rooms we're actually implementing (since we 
     *   described it as a sprawling mansion from the outside). Note that 
     *   the PC can't actually travel via this connector under any 
     *   circumstances, but that different reasons will be given depending 
     *   on whether the alarm is on or off; while the alarm is on the 
     *   travelBarrier will take precedence over the travelDesc message.
     */
    
    south: FakeConnector 
    { 
        "That way leads to the kitchen, but you don't need to eat just at the
        moment. " 
        travelBarrier = [alarmBarrier]
    }
    up = hallStairs
;

/*  
 *   LOCKABLE WITH KEY, DOOR 
 *
 *   This is the other side of the outside of the front door, defined above; 
 *   and the definition is much the same.
 */

+ frontDoorInside: LockableWithKey, Door 'front door' 'front door'
    keyList = [doorKey, skeletonKey]
;

/*  
 *   STAIRWAY UP & FAKE CONNECTOR
 *
 *   We can combine FakeConnector and StairwayUp to make a staircase that 
 *   will never be climbed. While the alarm is on the alarmBarrier will take 
 *   precedence for explaining why not, but once it's off the travelDesc will
 *   provide the expanation. 
 *
 *   Again, the purpose of this staircase is simply to suggest that the 
 *   house is larger inside than we've really made it.
 */

+ hallStairs: FakeConnector, StairwayUp 'broad flight/stairs' 'flight of stairs'
   travelDesc = "Upstairs you'll find only bedrooms and bathrooms, and you
       don't want to sleep or wash right now. "      
    
    travelBarrier = [alarmBarrier]
;

/*   
 *   KEYED CONTAINER
 *
 *   A KeyedContainer does need a key to lock it and unlock it. We define 
 *   this one so that either the skeleton key or the silver key will unlock 
 *   it.
 */

+ KeyedContainer, CustomFixture 'incongruously small modern white box' 'white box'
    "It's quite small, and is fitted at about shoulder height just next to the
    front door. "
    cannotTakeMsg = 'The box is firmly fixed to the wall. '
    keyList = [silverKey, skeletonKey]
    material = paper
;

/*  
 *   CUSTOM FIXTURE
 *
 *   This panel object is used to represent the innards of the burglar alarm 
 *   control box. Note that we override isListedInContents and isListed so 
 *   that this panel is clearly announced as being in the box once the box is
 *   opened.
 */

++ alarmPanel: CustomFixture 'alarm panel/keypad' 'alarm panel'
    "It has a keypad with ten buttons, numbered 0 to 9. <<isOn ? 'A red light
        is flashing on the panel' : 'The red light is off'>>. "
    isListedInContents = true
    isListed = true
    
    /* 
     *   Here we provide the code for turning off the alarm by typing the 
     *   correct code on the keypad. Note that the combination matches the 
     *   date on the door lintel outside, but by defining a custom 
     *   combination property we make it easy to change it to anything we 
     *   like.
     */
    
    combination = '1908'
    dobjFor(TypeLiteralOn)
    {
        verify() {}
        
        /*  
         *   Since the keypad is described as having buttons numbered 0 to 9 
         *   we need to rule out the attempt to type any other characters on 
         *   it.
         */
        
        check()
        {                        
            for(local i=1; i <= gLiteral.length; i++)                
            {
                local cur = gLiteral.substr(i, 1);
                if(cur < '0' || cur > '9')
                {
                    failCheck('You can only type numbers on the keypad; <q>'
                              + cur + '</q> isn\'t a number. ');
                }
            }
        }
        
        /*  
         *   If what the player types matches the combination, turn off the 
         *   alarm.
         */        
        action()
        {
            "Okay, you type <<gLiteral>> on the keypad";
            if(gLiteral == combination)
            {
                "; the red light stops flashing and the beeping stops";
                alarmPanel.isOn = nil;
            }
            ". ";
        }
    }
    isOn = true
    
    /* Make ENTER XXXX ON KEYPAD equivalent to TYPE XXX ON KEYPAD */
    
    dobjFor(EnterOn) asDobjFor(TypeLiteralOn)

;

/*  
 *   BUTTON
 *
 *   Our example of the Button class has a couple of little tricks to it. 
 *   First of all, when it's pressed all it does is to tell the player to 
 *   try typing on the keypad instead (so instead of typing PUSH BUTTON 1, 
 *   PUSH BUTTON 9, PUSH BUTTON 0, PUSH BUTTON 8, they just need to type 
 *   TYPE 1908 ON KEYPAD). Secondly we make one Button object represent all 
 *   10 buttons. As we've defined the vocabWords below our button will match 
 *   BUTTON 0, BUTTON 1, and so on all the way up to BUTTON 9. So whichever 
 *   (valid) button the player tries to press s/he'll be told to type on the 
 *   keypad instead.
 */

+++ Button, Component '0 1 2 3 4 5 6 7 8 9 small button/key*buttons*keys' 
    'buttons'
    "There are ten small buttons, numbered 0 to 9. "
    dobjFor(Push)
    {
        action()
        {
            "Instead of pushing individual buttons, just TYPE number ON KEYPAD.
            ";
        }
    }
;

/*   
 *   COMPONENT
 *
 *   This simply represents the red light that's mentioned in the 
 *   description of the alarm panel.
 */

+++ redLight: Component 'red light' 'red light'
    "It's <<alarmPanel.isOn ? 'flashing' : 'off'>>. "
;


/*  
 *   SIMPLE NOISE
 *
 *   Until the alarm is switched off it beeps continuously. The SimpleNoise 
 *   represents the beep. Until the alarm is switched off the player will be 
 *   told that "A beeping comes from the white box" on every turn. This will 
 *   also be the response to LISTEN, or LISTEN TO BEEP or LISTEN TO BOX.
 */

++ SimpleNoise 'beeping sound/beep' 'beeping'
    "A beeping sound comes from the white box. "
    
    /* 
     *   We want this sound to be mention even when the player doesn't 
     *   explicitly LISTEN.
     */
    isAmbient = nil
    
    /*  We want this sound to stop once the alarm is switched off. */
    isEmanating = (alarmPanel.isOn)
    
    /*  We want this sound to be mentioned every turn that the alarm is on.*/
    displaySchedule = [1]
;

/*  
 *   CUSTOM IMMOVABLE
 *
 *   The hatstand provides a rather implausible excuse for illustrating a 
 *   spring lever (see below). We make it Immovable rather than a Fixture, 
 *   say, because it's not clearly impossible for the PC to take the 
 *   hatstand, we'll just rule it out as pointless instead.
 */


+ hatStand: CustomImmovable 'tall old wooden (hat) stand/hatstand/hat-stand'
    'hat-stand'
    "It's a tall wooden stand, with a number of pegs for hanging hats on. There
    are no hats on the stand at the moment, but closer examination suggests that
    one of the pegs is hinged. "
    specialDesc = "An old wooden hatstand lurks to one side. "
    cannotTakeMsg = 'You don\'t want to be encumbered with it, so you may as
        well leave it where it is. '
;

/*  
 *   SPRING LEVER
 *
 *   As SpringLever is a lever that returns to its original position when it 
 *   is released, making it functionally equivalent to a Button. This 
 *   somewhat contrived example of a SpringLever drops the alarm box key 
 *   onto the floor when it is first pulled. It hardly matters if the player 
 *   doesn't discover this since the skeleton key in the black case will do 
 *   the job just as well.
 */

++ peg: SpringLever, Component 'hinged peg*pegs' 'hinged peg'
    dobjFor(Pull)
    {
        action()
        {
            if(silverKey.isIn(nil))
            {
                "When you pull the peg a silver key drops onto the floor. ";
                silverKey.makePresent();
            }
            else
                "You pull the peg but it springs back into place when you let
                go again. ";
        }
    }
    
;

/*  
 *   Another KEY. Also PRESENT LATER
 *
 *   By making the silver key also a PresentLater, we can make it appear on 
 *   the scene only when the peg is first pulled.
 */

+ silverKey: PresentLater, Key 'small silver key*keys' 'small silver key'    
;

//------------------------------------------------------------------------------
/*  ROOM */

study: Room 'Study'
    "A large wooden desk stands in the middle of the room, facing a television
    in the corner. The way out to the hall lies west, but to the north a
    door-sized panel has been set into the wall. "
    west = hall
    out asExit(west)
    north = panel
;

/*  
 *   INDIRECT LOCKABLE, DOOR
 *
 *   An IndirectLockable is something that is locked and unlocked by some 
 *   mechanism other than a key. We'll meet the odd mechanism for unlocking 
 *   this door below.
 */

+ panel: IndirectLockable, Door 'door-sized panel' 'panel'
    "It's about the shape and size of a door, but there's no handle or lock --
    at least, none visible. "
;

/*  
 *   SURFACE, HEAVY
 *
 *   A study ought to have a desk it it, if nothing else, so we'll provide 
 *   one. This desk will have a drawer that contains a clue to finding the 
 *   safe and opening it.
 */

+ Surface, Heavy 'large wooden desk/top' 'desk'
    "The baron must have very tidy working habits, since the top of his desk
    looks <<contents.length() > 0 ? 'almost' : ''>> entirely bare. You note,
    however, that the desk has a drawer. "
    
    /* 
     *   Redirect opening, closing, locking, unlocking and looking in to the 
     *   drawer.
     */
    dobjFor(Open) remapTo(Open, drawer)
    dobjFor(Close) remapTo(Close, drawer)
    dobjFor(LookIn) remapTo(LookIn, drawer)
    dobjFor(Unlock) remapTo(Unlock, drawer)
    dobjFor(Lock) remapTo(Lock, drawer)
    dobjFor(UnlockWith) remapTo(UnlockWith, drawer, IndirectObject)
    dobjFor(LockWith) remapTo(LockWith, drawer, IndirectObject)
;

/*  
 *   KEYED CONTAINER
 *
 *   The drawer is another KeyedContainer. Again it can be unlocked either 
 *   with its own key or with the PC's skeleton key.
 */

++ drawer: KeyedContainer, Component 'small drawer' 'drawer'
    "It's not very big. "
    keyList = [skeletonKey, drawerKey]
    dobjFor(Pull) asDobjFor(Open)
    dobjFor(Push) asDobjFor(Close)
    
;

/*  
 *   OPENABLE 
 *
 *   Most openable objects will be either doors or containers, but a few 
 *   other things can be opened as well, such as books. To illustrate this 
 *   we'll make this notebook an Openable.
 */

+++ notebook: Openable, Readable 'small red notebook/book' 'small red notebook'
    "It's a small red notebook. "
    readDesc = "Most of the pages are blank, but towards the back you find
        someone has written <q><<tvDial.advertising>> is safe:
        <<dial.combination>></q>"
    dobjFor(Read) { preCond = [objHeld, objOpen] }
;

/*  
 *   COMPLEX CONTAINER
 *
 *   The small wooden box is going to be used to illustrate the Settable 
 *   class (in the form of a slider used to unlock it). Note that we have to 
 *   make the box a ComplexContainer, because we're going to add an external 
 *   component; if we made smallBox an lockable container directly (a very 
 *   easy trap to fall into) we'd find that the external component (the 
 *   slider) actually ended up locked up inside the box, where it would be 
 *   permanently inaccessible.
 */

++ smallBox: ComplexContainer 'small carved wooden box' 'small wooden box'
    "It is delicately carved, and has a strange slider on one side. "
    
    /* 
     *   INDIRECT LOCKABLE
     *
     *   The subContainer provides an example of an IndirectLockable 
     *   OpenableContainer, that is a container locked and unlocked by some 
     *   means other than a key.
     */
    
    subContainer: ComplexComponent, IndirectLockable, OpenableContainer
    {
        bulkCapacity = 2
    }
;

/*  
 *   KEY
 *
 *   Inside the small wooden box we put the key to the desk drawer. 
 */

+++ drawerKey: Key 'small gold key' 'small gold key'
    subLocation = &subContainer
;

/*   
 *   SETTABLE
 *
 *   As an example of the base Settable class we'll implement a slider on the
 *   outside of the box that's used to unlock it. To unlock the box the 
 *   player must spell out OPEN with the initial letters of the composers' 
 *   names. It doesn't matter here that this isn't a particularly fair 
 *   puzzle (a) because the player can always use the skeleton key instead 
 *   and (b) this is only a demo, after all, and the point is to illustrate 
 *   the Setter class, not to create a great puzzle.
 *
 *   It's more important that Settable only provides a framework, so that to 
 *   make it work as a slider we shall have to do quite a bit of the work 
 *   ourselves.
 */
 

+++ slider: Settable, Component 'slider/pointer' 'slider'
    "The slider has a pointer which can be set to one of the names engraved
    along its length (which all seem to be names of composers): Elgar, Nielsen,
    Offenbach or Pachabel. It's currently set to <<curSetting>>. "
    
    curSetting = 'Elgar'
    
    /* 
     *   This is not a standard library property on Settable (although it is 
     *   on some of Settable's subclasses); it's one we're defining for 
     *   ourselves.
     */
    validSettings = ['ELGAR', 'NIELSEN', 'OFFENBACH', 'PACHABEL']
    
    
    /*
     *   We override the canonicalizeSetting() method to convert the player's
     *   input to upper case. This makes it easier to compare with the list 
     *   of valid settings.
     */
    canonicalizeSetting(val)
    {
        return val.toUpper();
    }
    
    /*
     *   We override the isValidSetting() method to return true only if the 
     *   setting attempted by the player is one of those in the list of 
     *   validSettings.
     */
    
    isValidSetting(val)
    {
        return validSettings.indexOf(val) != nil;        
    }
    
    /*  
     *   We define a custom settingHistory property to contain a record of 
     *   the initial letters of the last four settings the player moved the 
     *   slider to (so we can check if this ever spells 'OPEN')
     */
    
    settingHistory = ''
    
    makeSetting(val)
    {
        inherited(val);
        
        /* Add the first letter of the setting string to the settingHistory */
        settingHistory += val.substr(1,1);
        
        /* 
         *   If settingHistory is longer than four letters, keep only the 
         *   last four letters
         */
        if(settingHistory.length() > 4)
            settingHistory = settingHistory.substr(-1, 4);
            
    }
    
    dobjFor(SetTo)
    {
        action()
        {
            inherited();
            
            /* 
             *   If the latest setting makes the last four settings 
             *   (including the latest) spell OPEN then unlock the box; 
             *   otherwise lock it.
             */
            if(settingHistory == 'OPEN')
            {
                smallBox.makeLocked(nil);
                "As you set the pointer to <<curSetting>> you hear a faint click
                from the box. ";
            }
            else
                smallBox.makeLocked(true);
        }
                
    }
;

/*  
 *   HEAVY 
 *
 *   A television may seem a highly unlikely device for opening a door, but 
 *   Baron von Epresser is a bit of a weirdo, and the TV allows us to 
 *   illustrate a few more devices and contraptions.
 */

+ tv: Heavy 'television/tv/telly/screen' 'television'
    "Beneath the screen the TV has a switch and a dial. <<tvSwitch.isOn ?
      reportResponse(tvDial.curSetting, nil) : 'The screen is currently
          blank. '>>"
    
    /* 
     *   The reportResponse() method shows what's on the TV screen when the 
     *   dial is turned to val. If trigger is true it also unlocks and opens 
     *   the panel when val is 'advertising' (we don't want this effect when 
     *   reportResponse() is called from desc(), i.e. when the player is 
     *   simply examining the TV).
     */
    
    reportResponse(val, trigger)
    {
        
        "The screen shows ";
        switch(val.toLower())
        {
            case 'sport': 
            case 'sports': "a football match. "; break;
            case 'soap': "episode 34,954,221,345 of the world's longest running
                soap-opera, in which the Amoeba family are still quarreling
                over evolutionary challenges. ";
            break;
            case 'news': "a news broadcast that looks even more depressing than
                usual: your least favourite political party is 12 points ahead
                in the polls, interest rates are set to double, and every key
                public sector worker is going on strike indefinitely pending the
                grant of the unions' demands for 53 weeks' holiday a year."; 
            break;
            case 'weather': "the latest weather forecast: scattered showers,
                sunny periods, hail, snow, heatwave, drought, and floods at
                various times and sundry odd places. "; break;
            case 'drama': "a particularly gory production of <i>Hamlet</i>. "; break;
            case 'music': "a classical concert -- one of Mahler's extra-long
                symphonies by the look of it. "; break;
            case 'advertising': 
            case 'home shopping': "a series of advertisements for useless items
                you never knew you wanted and certainly can't afford. "; 
            if(trigger)
            {
                "The panel slides <<panel.isOpen ? 'shut' : 'open'>>. ";
                panel.makeLocked(!panel.isLocked);
                panel.makeOpen(!panel.isOpen);                    
            }
            
            break;
            default: "little of interest. "; break;
        }
    }
    
    dobjFor(TurnOn) remapTo(TurnOn, tvSwitch)
    dobjFor(TurnOff) remapTo(TurnOff, tvSwitch)
;

/*  
 *   SWITCH 
 *
 *   We could simply have made the TV a switch, since most people will 
 *   probably try to turn it on an off directly (which we also allow through 
 *   the remap statements above). But since the description of the TV refers 
 *   to it as a separate object we may as well implement it separately.
 *
 *   Note that Switch is a subclass of OnOffControl and behaves almost 
 *   identically except that it also responds to the commands SWITCH and 
 *   FLIP, which toggle it between its on and off states. Since the two 
 *   classes are so similar we shall not provide a separate example of an 
 *   OnOffControl.
 */

++ tvSwitch: Switch, Component 'on-off switch' 'switch'
    "It's just a simple on-off switch. "
    makeOn(val)
    {
        inherited(val);
        if(val)
            tv.reportResponse(tvDial.curSetting, true);
        else
            "The screen goes blank. ";
    }
;

/*  
 *   LABELED DIAL
 *
 *   A LabeledDial is a specialization of Settable, representing a dial that 
 *   can be turned to a number of author-defined settings. In general 
 *   there's less work in defining a LabeledDial than a Settable, since more 
 *   of the work is already done by the library.
 */

++ tvDial: LabeledDial, Component 'dial' 'dial'
    "The dial can be turned to <<listSettings()>>; it's currently turned to
    <<curSetting>>. "
    
    /* 
     *   validSettings is a library property on LabeledDial, but we need to 
     *   define what the valid settings are.
     *
     *   Note that while five of the settings have been defined with literal 
     *   strings, two have been defined with properties of the dial object. 
     *   This incidentally shows that properties and strings can safely be 
     *   mixed in a list such as this, but it also shows how we might make 
     *   the list adaptive.
     */
    validSettings = [sport, 'Soap', 'News', 'Weather', 'Drama',
        advertising, 'Music' ]
    
    
    curSetting = validSettings[1]
    
    /*  
     *   listSettings() is a custom method we are defining here so that the 
     *   description of the dial will automatically (and accurately) reflect 
     *   the validSettings defined above.
     */
    listSettings()
    {
        foreach(local cur in validSettings)                
        {            
            if(validSettings.indexOf(cur) == validSettings.length())
                "or <<cur>>";
            else
                "<<cur>>, ";
        }
    }
    makeSetting(val)
    { 
        inherited(val);
        
        /* 
         *   If the TV is on, we need to change what it displays in 
         *   accordance with the new setting, and report the change.
         */
        if(tvSwitch.isOn)
            tv.reportResponse(val, true);
    }    
    
    /*  
     *   We define these two settings through properties since British and 
     *   American players might expect to see them described differently.
     */
    
    advertising = (me.nationality == british ? 'Advertising' : 'Home Shopping')
    sport = ( me.nationality == british ? 'Sport' : 'Sports')
;





//------------------------------------------------------------------------------
/* ROOM */

cubbyHole: Room 'Cubby Hole'
    "This really is no more than a tiny cubby hole, with fully half the space
    taken up with a huge safe. The only other feature of interest here is the
    green lever set in the wall, next to the sliding panel to the south. "
    south = cubbyPanel
    out asExit(south)
;

/*  
 *   INDIRECT LOCKABLE, DOOR
 *
 *   The cubby panel represents the other side of the panel in the study. It 
 *   too is an indirect lockable, but we'll provide a different (and simpler)
 *   mechanism for locking and unlocking it from this side.
 */

+ cubbyPanel: IndirectLockable, Door  -> panel 'sliding panel' 'panel'
;

/*   
 *   LEVER
 *
 *   A Lever can be in one of two states: pushed and pulled. It can then be 
 *   pulled and pushed respectively to change states. Here we use a Lever to 
 *   provide a simple mechanism for opening/unlocking and closing/locking the
 *   sliding panel from inside the cubby hole.
 */

+ Lever, Fixture 'green lever' 'green lever'
    "It's set at a convenient height in the wall. "
    makePulled(stat)
    {
        cubbyPanel.makeOpen(stat);
        cubbyPanel.makeLocked(!stat);
        "The panel slides <<stat ? 'open' : 'closed'>>. ";
    }
;



/*  
 *   COMPLEX CONTAINER
 *
 *   Like the small wooden box on the desk above, the safe needs to be 
 *   implemented as a ComplexContainer since it has an exterior component, in
 *   this case the dial used to unlock it.
 */

+ safe: ComplexContainer, Heavy 'huge safe/door' 'safe'
    "Apart from looking huge and impregnable, the most interesting feature of
    the safe is the black dial set in the middle of its door. "    
    subSurface: ComplexComponent, Surface { }
    subContainer: ComplexComponent, IndirectLockable, OpenableContainer 
    {
        cannotLockMsg = 'Presumably, you have to use the dial. '
        cannotUnlockMsg = (cannotLockMsg)
    }
    
;

/*   
 *   NUMBERED DIAL
 *
 *   NumberedDial is another specialization of Settable. As its name 
 *   suggests it can be used to represent a dial that can be turned to a 
 *   range of mumeric values. Here we use it for a classic combination lock.
 */


++ dial: NumberedDial, Component 'black dial' 'black dial'
    "It's a black dial which can be turned to any number from 0 to 99; it's
    currently turned to <<curSetting>>. "
    
    /* 
     *   The following three properties are standard library properties for a
     *   NumberedDial. Note the oddity that while minSetting and maxSetting 
     *   have to be defined with integer values, curSetting has to be 
     *   defined (and used) as a string.
     */
    minSetting = 0
    maxSetting = 99
    curSetting = '15'
    
    /*   
     *   Since we're using the dial as a combination lock, we'd better give 
     *   it a combination. Again, this is a string property (since it will be
     *   matched against values of curSetting, which is a string). 
     */
    combination = '239756'
    
    /*   
     *   We also need a property to store the numbers to which the dial has 
     *   been turned, so that we can tell when the correct combination has 
     *   been entered.
     */
    numbersDialled = ['0','0','0']
    
    
    dobjFor(SetTo)
    {
        action()
        {
            inherited;
            
            /* Keep a record of the last three numbers dialled */
            numbersDialled[1] = numbersDialled[2];
            numbersDialled[2] = numbersDialled[3];
            numbersDialled[3] = curSetting;
            
            /* 
             *   If the last three numbers dialled match the combination, 
             *   unlock the safe, otherwise lock it if it is closed.
             */
            
            if(numbersDialled[1] + numbersDialled[2] + numbersDialled[3] ==
               combination)
            {
                safe.subContainer.makeLocked(nil);
                "As you turn the dial to <<curSetting>>, a satisfying
                <i>click</i> comes from the safe. .";
            }
            else if(!safe.subContainer.isOpen)
            {
                safe.subContainer.makeOpen(nil);
            }
        }
        
    }
;

/*   
 *   READABLE 
 *
 *   Finally, we implement the letter the PC has come to retrieve. In a real 
 *   game we'd doubtless want to put other things in the safe as well, but 
 *   since this is a demo the letter alone will do.
 */

++ letter: Readable 'love incriminating letter/love-letter' 'letter'
    "One glance suffices to tell you that this is the letter you came to
    recover: a youthful indiscretion, written to an inappropriate lover. You
    have no desire to read it through; merely remembering it is quite
    embarrassing enough. Once you're out of here you'll destroy it. "
    subLocation = &subContainer
;

//==============================================================================

modify finishOptionAmusing
    doOption()
    {
        if(!me.hasSeen(skeletonKey))
            "Try looking in the black case you're carrying.\b";
        
        if(!me.hasSeen(doorKey))
            "Try looking under the flowerpot.\b";
        
        if(torch.isInInitState)
            "Try calling the plastic tube by its proper name next time. ";
        
        if(torch.name == torch.britishName)
            "Next time, see what happens if you call the torch a flashlight.\b";
        
        if(torch.name == torch.americanName)
            "Next time, see what happens if you call the flashlight a torch.\b";
        
        if(!me.hasSeen(silverKey))
            "Try taking a closer look at the hat-stand in the hall and see if
            there's anything you can pull on it.\b";
        
        if(!me.hasSeen(drawerKey))
            "Take a closer look at the box on the desk and see if you can get
            it to OPEN.\b";
        
        "Try turning the dial on the TV to see what's on the various
        channels.\b";
        
        
        /* 
         *   We need to return true to tell the caller that we've done with 
         *   this option and we want to display the list of options again.
         */
        return true;
    }
;