adv3lite: A Hidden Button

I just stumbled on this little problem. I haven’t thought it through yet, so possibly I can come up with a fix or a workaround, but maybe someone will have a suggestion. For the puzzle I’m working on … well, this is going to be a bit of a spoiler, no way around it.

There’s a button on the underside of a desk. In order to open a particular door, you need to look under the desk so as to discover the button, and then press the button. The real puzzle is somewhat more complex than that, but that’s the essence of the action.

Just about the only property of the Button class is that isFixed = true. This is obviously necessary, as this particular button is not something the player will be able to carry around. But the result is that ‘look under desk’ does not mention that the button is there – because it’s essentially a Fixture. It’s defined using + notation and subLocation = remapUnder, so I know it’s where it’s supposed to be.

In the interests of verisimilitude, the player would naturally be able to put other, irrelevant things under the desk. So I tried this:

dobjFor(LookUnder) { action() { "Mounted on the lower surface of the desktop is a small metal button. "; inherited(); } }
But that doesn’t work at all. Edit: It does work, when the dobjFor(LookUnder) is in the SubComponent. But now I need a refinement of the output message. If the button is the only thing under the desk, I need, “You find nothing else of interest under the desk.” That is, I need to splice the word “else” into the output message. If there is something else under the desk, I need to splice the word “also” into the output – “Under the desk you also see…”

Time to look into message customization, I guess.

One way to do it is to define a desk object and a deskUnderside object which is a subcomponent of the desk.

In the SubComponent implementation under desk, do a dobjFor(LookUnder) that provides conditional text depending on what may or may not be present in addition to the button.

Here’s how it looks in a room that has the desk with a button mounted to the underside, plus a box. If you look under the desk, you see the button and are told there is nothing else there. If you put the box under the desk and then look under it, you are told about the button and the box…

Here’s the code…

#charset "us-ascii"

#include <tads.h>
#include "advlite.h"

versionInfo: GameID
    IFID = '445C38A3-AD1B-4729-957A-F584600DE5C1'
    name = 'test'
    byline = 'by Jerry Ford'
    htmlByline = 'by <a href="mailto:jerry.o.ford@gmail.com">
                  Jerry Ford</a>'
    version = '1'
    authorEmail = 'Jerry Ford <jerry.o.ford@gmail.com>'
    desc = 'Testing LookUnder message'
    htmlDesc = 'Testing LookUnder message.'

;

gameMain: GameMainDef
    initialPlayerChar = me
    paraBrksBtwnSubcontents = nil
   
;

me: Actor 'me' @livingRoom
    "The main man.<.p>"
    isHim = true
    person = 2
;

livingRoom: Room 'The Living Room' 
    "The living room. There is a desk here."
;

+ box: Thing 'box'
    "A box."
;

+desk: Thing, Fixture 'desk'
    "The desk."
    
    remapUnder: SubComponent 
    {
        dobjFor(LookUnder)
        {
            action()
            {
                "There is a hidden button mounted to the underside of the desk.<.p>";
                if(self.contents.length == 0)
                    "There is nothing else of interest here.<.p>";
                else
                    inherited;
            }
        }
    }
;
++ deskUnderside: Surface, Fixture 'underside of desk'
    "The desk underside."
    
    SubLocation = &remapUnder
    
;
+++button: Button, Fixture 'button'
    "A hidden button"
    isListed = nil
;

Jerry

That would work, but it doesn’t read well. What I want is a single paragraph that reads, “There is a button mounted to the underside of the desk. Under the desk you also see a box.” I’m kind of a fanatic about having a game actually produce output that reads as if it were written by a human writer. (Possibly because I’m a human writer.)

I emailed Eric about this. I can’t find the BMsg or DMsg that produces “Under the desk you see a box.” I searched the entire Messages list for both “under” and “see”, and there doesn’t seem to be a message that produces this output. Do you happen to know where it’s produced?

Edit: It seems to be coming from the lookInLister (which is presumably built into the object). But “Learning T3Lite” shows only how to create CustomRoomLister objects, and a room lister is not what I want. I don’t know how to create a custom lister for a specific container.

No. I don’t think it’s generated in a place where you will be able to intercept it. A grep of the adv3Lite library for both "nder the " and " find " did not produce that message. I suspect this message is generated at a much lower level.

As for the sentence that is output, you do have all the building blocks you need to make it anything you want. It just depends on how much work you want to put into it. In the example code I gave you, in dobjFor(LookUnder), all of the items placed under the desk are listed in the self.contents list.

If you are determined to make the output text your own, you could construct a sentence along these lines:

if(self.contents.length > 0)
{
    "You see ";

    local itemCount = self.contents.length;
    local count = 1;
    for(count;count <= itemCount; count++)
    {
        local item = self.contents[count].name;
        if(rexMatch(R'%<[aeiouAEIOU]'), item)
            item = 'an ' + item;
        else
            item = 'a ' + item;
        if(count == itemCount)
        {
            ", and ";
            say(item);
            ". <.p>";
         }
         else
        {
             say(item); 
             ", ";
        }
}

Note that I have not tried to compile this code. It will take some experimenting to get the words to flow correctly. And I am not at all sure about that regex for identifying words that start with a vowel. That’s just a shot off the top of my head for illustration sake.

But the principle should hold up, just depends on how determined you are to avoid the game-generated text.

Jerry

Check this out, it’s in the TADS Technical Manual…

file:///TADS3/doc/techman/t3lister.htm

Jerry

So I was curious to know if my coded sentence works and it does…

It took a couple of minor tweaks (syntax of the rexMatch call was off—misplaced paren—but the regex itself was correct; I also had an extra comma before the “and”.

For the record, here’s the code that works…

                    if(self.contents.length > 0)
                    {
                        "You see ";

                        local itemCount = self.contents.length;
                        local count = 1;
                        for(count;count <= itemCount; count++)
                        {
                            local item = self.contents[count].name;
                            if(rexMatch(R'%<[aeiouAEIOU]', item))
                                item = 'an ' + item;
                            else
                                item = 'a ' + item;
                            if(count == itemCount)
                            {
                                "and ";
                                say(item);
                                ". <.p>";
                             }
                             else
                            {
                                 say(item); 
                                 ", ";
                            }
                        }
                    }

Yeah, I was looking at that while I ate supper. I think the message is being constructed by a lister, all right. But the class hierarchy of Listers in adv3, and the ways in which they’re summoned by the adv3 library, seems not to be the same as what happens in adv3Lite. As far as I can see from a quick, cursory examination of the adv3Lite Library Reference Manual, many of those listers don’t exist. Mostly adv3Lite just uses slight tweaking of the Thing class for almost every object. The Thing class has an examineLister property … but whether that’s the one that ‘look under’ invokes, I don’t know.

Okay, I got kind of carried away, I admit. But you posed an interesting question, and I can see that the end result will be a useful addition to my own toolkit, so I plunged ahead and resolved the issues I saw in my solution, presented yesterday, to the problem of listing the contents of a subcomponent.

I have moved the code out of the dobjFor(LookUnder) macro and made it into an anonymous function, so that it can be used elsewhere simply by passing it a list. The contents of that list are returned in the form a single-quoted string.

Here is the result in a game window, where two bulls, three balls, and an apple are placed under a desk…

Here is the code that produces it…

#charset "us-ascii"

#include <tads.h>
#include "advlite.h"

versionInfo: GameID
    IFID = '445C38A3-AD1B-4729-957A-F584600DE5C1'
    name = 'test'
    byline = 'by Jerry Ford'
    htmlByline = 'by <a href="mailto:jerry.o.ford@gmail.com">
                  Jerry Ford</a>'
    version = '1'
    authorEmail = 'Jerry Ford <jerry.o.ford@gmail.com>'
    desc = 'Testing LookUnder message'
    htmlDesc = 'Testing LookUnder message.'

;

gameMain: GameMainDef
    initialPlayerChar = me
    paraBrksBtwnSubcontents = nil
   
;

me: Actor 'me' @livingRoom
    "The main man.<.p>"
    isHim = true
    person = 2
;

livingRoom: Room 'The Living Room' 
    "The living room. There is a desk here."
;

+ brownBull: Thing 'bull;brown'
    "A brown bull.<.p>"
;
+ blackBull: Thing 'bull;black'
    "A black bull.<.p>"
;
+ redBall: Thing 'ball;red'
    "A red ball.<.p>"
;
+ yellowBall: Thing 'ball;yellow'
    "A yellow ball.<.p>"
;
+ greenBall: Thing 'ball;green'
    "A green ball.<.p>"
;
+ apple: Thing 'apple'
    "An apple. <.p>"
;

+desk: Thing, Fixture 'desk'
    "The desk."
    
    remapUnder: SubComponent 
    {
        dobjFor(LookUnder)
        {
            action()
            {
                "There is a hidden button mounted to the underside of the desk";
                if(self.contents.length == 0)
                    ". There is nothing else of interest here.<.p>";
                else
                {
                    if(self.contents.length > 0)
                    {
                        ", as well as ";
                        say(list2text(self.contents));
                    }
                }
            }
        }
    }
;
++ deskUnderside: Surface, Fixture 'underside of desk'
    "The desk underside."
    
    SubLocation = &remapUnder
    
;
+++button: Button, Fixture 'button'
    "A hidden button"
    isListed = nil
;


list2text(obj)
{
    local xfrList = [];
    local retList = [];
    local str = '';
    if(obj.length > 0)
    {
        // add "a" or "an" to each item in the list
        local itemCount = obj.length;
        local count = 1;
        local item = '';
        for(count;count <= itemCount; count++)
        {
            item = obj[count].name;
            if(rexMatch(R'%<[aeiouAEIOU]', item))
                item = 'an ' + item;
            else
                item = 'a ' + item;
            xfrList = xfrList + item;
        }
        
        // aggregate multiple listings of identical items... 
        // ['2 bulls', '3 balls']
        // instead of 
        // ['a bull', 'a bull', 'a ball', 'a ball', 'a ball']
        count = 1;
        itemCount = xfrList.length;
        while(count <= itemCount)
        {
            local checkCount = 1;
            local matchCount = 0;
            local matchCountStr = '';
            item = xfrList[count++];
            for(checkCount = 1; checkCount <= itemCount; checkCount++)
            {
                if(item == xfrList[checkCount])
                    matchCount++;
            }
            if(matchCount > 1)
            {
                matchCountStr = toString(matchCount) + ' ';
                item = matchCountStr + item.split(' ')[2] + 's';
                count += matchCount-1;
            }
            retList = retList.append(item);
        }
        
        // turn new list into a string
        itemCount = retList.length;
        count = 1;
        for(count;count <= itemCount; count++)
        {
            item = retList[count];
            item = item;
            if(count == itemCount)
            {
                if(count > 1)
                    str += 'and ';
                str += item + '. <.p>';
             }
             else
            {
                 str += item + ', ';
            }
        }
    }
    return str;
}

If you try this out and see issues I have missed, please let me know. I’d like to fix it. As noted, it’s for my own use as much as anything, though I’m happy to share. :slight_smile:

Jerry

The fact that a Button defines isFixed = true doesn’t stop you from overriding its isListed property to true so that it still shows up in a listing, and you could then give it a specialDesc if you liked to control how it’s described. This might be simpler than some of the other solutions!

Alas, this doesn’t work. If I add isListed = true to the button, it shows up in the Room description, which is NOT what’s wanted. If I also add isHidden = true, the player won’t be able to do anything with the button until I change that to isHidden = nil – and once I do that, the button shows up in the room description, even though it’s on the underside of the desk.

I could probably cobble together some sort of logic for isListed that would return nil if the gActionIs whatever happens when you look around the room or examine the desk, but I’m not sure how that code would be organized … and indeed, it doesn’t seem to work. Here’s what I tried:

isListed = ((gActionIs(Look) || (gActionIs(Examine) && (gDobj == floggDesk))) ? nil : true)

The button still shows up in response to both the Look action and ‘examine desk’.

If you run the code I posted a couple of posts back, this will not occur.

The button is never mentioned until you explicitly look under the desk.

In that code, you can push the button at any time even though you haven’t seen it. I tried adding an if(!button.seen) condition to the check() method of dobjFor(Push) and that didn’t work—turns out both button.seen and button.known are always true even though it has not yet been discovered, but I solved that easily enough by adding my own discovered = nil property to the button, making the check() condition if(!button.discovered), and setting discovered to true in the dobjFor(LookUnder} macro.

Now it works fine, the way I understand you to say you want it to work—no mention of the button and button not usable until you look under the desk, plus you get the ability to customize the “also under the desk” text.

Jerry

I’m sure that’s true. What struck me about your code is that you’re essentially recreating the functionality of a Lister by hand.

One of my goals in the game I’m working on – admittedly not a major part of the goal, but not trivial either – is to work my way through the functionality of adv3Lite, in order to possibly spot a few minor snarfles in the library so Eric can ponder them and decide if he wants to make adjustments. If I avoid using the lookInLister and CustomMessages (a usage that Eric has suggested to me in private correspondence), I’ll defeat this purpose. Plus, my assumption is that in designing the library he devoted a lot of attention to “edge cases,” which might or might not be handled by your code. If I use your code, I’ll have to repeat the process of testing whether those edge cases cause problems. For both reasons, I plan to stick with vanilla adv3Lite wherever possible.

I appreciate that you’re doing cool stuff with TADS 3 coding – stuff that I would struggle to develop for myself. I hope you don’t take this the wrong way, as I admire your industry and expertise. But for now, I have my own agenda, as described above.

As indicated in our email correspondence, I misunderstood what you were trying to do. To have the button mentioned when the player character looks under the desk but not in a room description, set its searchListed property to true. You could then also use isHidden to prevent any access to it until the player has found it by looking under the desk, or alternatively you could start the button out in the desk’s hiddenUnder list (so that it will be automatically moved to the desk’s underside SubComponent when the player looks under the desk).

You may then find (again I’m not at home right now so I can’t try this) that you can customise the listing of objects under the desk by using code like this, without having to worry about the lookInLister:

button: Button 'button'
    searchListed = true
    specialDesc = "There is a hidden button fixed to the underside of the desk. 
    <<if location.contents.length > 1>> Under the desk you can also see 
    <<list of location.listableContents>><<end>>. "
    
    specialDescBeforeContents = true
;

This is essentially the technique described in the Library Manual for use with room descriptions, but hopefully it should also work here.

That’s very helpful, thanks. Now that I know you’re doing that I may hold off releasing the next update until you’ve finished, so I can include all the issues that need to be dealt with that turn up. I now have a critical mass of stuff that would justify a new release, but unless anyone urgently needs it, I can hang on to it for now. (And if anyone is desperate they can always download an intermediate version from GitHub ).

Well, this is an enormous project. Best-case scenario, it won’t be finished for six months. And knowing my own work habits as well as I do, I would guess I might get interested in something else and put the game on the back burner. I started writing this game in 2009 and did a little more work on it in 2011. My level of commitment today is higher than formerly, but – basically, I’m just saying, do what works for you. Maybe wait a few more weeks, as I’m finding a few little things, but don’t let my good intentions get in the way of your process.

Fair enough. I’ve reached a point where I could issue a new release, but I’m not in any hurry, and if you’re still finding stuff it’s probably better for me to hold off for a few weeks in case you find some more and then deal with it all in one update.

BTW, now I’ve had a chance to try it out, the correct code for a Button that reports itself and anything else under the desk the way you want would be as follows:

++ floggButton: Button 'button (under) (Flogg\'s) (desk); small (metal)'
     "Peering underneath the surface of the desk, you can see a small metal button. "
     subLocation = &remapUnder
    
    searchListed = true
    specialDescBeforeContents = true
    
    specialDesc = "Mounted on the lower surface of the desktop is a small metal
        button. <<if location.contents.length > 1>>Under the desk you
        can also see <<list of location.listableContents>>. "
    
    useSpecialDesc = (gAction && gActionIs(LookUnder))
;

With this code in place you don’t need to override dobjFor(LookUnder) on the desk’s underside SubComponent and you don’t need a CustomLister, since the button’s specialDesc effectively takes care of it.