Flags and variables in TADS3

Hello!

How is everything? My name is Alberto and this is my first post here :). I am an IF player since long ago, I started in the 80s when I was a kid with my Amstrad CPC and I have played quite a few titles, but mainly in microcomputers. My only contribution as an author to the IF world is an Amstrad game that was released in 2016, Doomsday Lost Echoes. This game was made between one of my best friends, that took care of all the pixel art, drawings, etc., and me working in the code. Lots of other people greatly helped during the development and beta testing period too. At the end of the day, it was quite a nice community project, I would say :).

DDLE was created using PAWS, I am sure that some of you still remember it. The whole game database, when compressed, fits in 61KB, but it would be around 80KB if uncompressed. The source code, including annotations, is just around 330KB. The graphics are loaded from the computer drive in real time, they are moved straight into the video memory region when the player changes rooms. As you can imagine, the whole game is quite simple, particularly because it was made with the intention of being accessible to everybody, even new IF players. To achieve this in a 8-bit computer puzzle difficulty is not very high and we made sure that several different commands can be typed to solve each of them.

These days I am trying to port our little adventure to TADS3. I thought that it would be a great way to get familiar with TADS and, given how simple is the game compared with those released in PC, I did not expect the port to be super challenging. Therefore, I am porting it while reading Learning T3, Getting started in TADS3 and the technical manuals. I thought that it could be a good idea to start the port while I read documentation in order to consolidate my knowledge with a practical project (on top of the examples proposed in the documentation).

Currently, I finished creating all the locations and the inventory items, and I am slowly modelling each of the rooms: the furniture, decoration, containers, etc., and putting the objects in their starting places. I am not planning to implement most of the puzzles until all this is done.

Thing is that while I was doing all this I tried to implement something a bit more advanced and it is not working as intended, probably because I went ahead too fast. Things like this, I thought that I could ask you guys about it and also ask if my general approach of tracking things is remotely right when using TADS3.

The “puzzle” I am trying to implement is super simple: the player grabs a battery from a container and puts it in the battery holder of a generator. This switches the generator on, something that activates several electrical systems around. Once the battery is in the holder the puzzle is over and the battery as such disappears. The holder also closes permanently and the generator is ON until the end of the game.

At the beginning I was just checking if the battery was inside the holder (implemented as a container) for things to happen. However, I thought that it could be more practical to create an object that holds a collection of variables to keep track of the status of the world. Therefore, I made something like this (yep, I know, I am not using templates):

//***Game flags***
flag_index: Thing
    vocabWords = '(flag) index'
    name = 'flag index'
    battery_inserted = 0
;

and I implemented the battery holder as below:

power_plant_generator_battery_holder: OpenableContainer, SingleContainer, RestrictedContainer, Fixture, Hidden
    vocabWords = '(battery) (generator) holder*holders/receptacle*receptacles/snap*snaps/enclosure*enclosures'
    name = 'battery holder'
    location = Power_plant
    validContents = [nuclear_Battery]
    isOpen = true
    desc()
    {
        if(flag_index.battery_inserted == 1)
        {
            "The battery holder is closed and locked. The battery is inside. ";
            cannotOpenMsg = 'I better leave it alone now that the generator is running. ';
        }
        if(flag_index.battery_inserted == 0)
        {
            "The battery holder is empty. These generators are plug and play, inserting the right battery on it should power it on. ";
        }
    }
    notifyInsert(nuclear_Battery, newCont)
    {
        "When I insert the nuclear battery in the holder I hear a click and the compartment closes. ";
        makeOpen(nil);
        flag_index.battery_inserted = 1;
        nuclear_Battery.moveInto(nil);
    } 
;

The logic is to change the value of “battery_inserted” in the “flag_index” object from 0 to 1 once the player puts the battery inside the holder, and then just check for the value of this variable to adjust descriptions and behavior of other things, like doors. However, the approach only partially works: the game outputs the right descriptions when the battery is inserted, but the battery as such does not disappear and the holder does not close.

My questions at this point are two:

  • Do you think that I am following a completely flawed approach to track the status of the world? I could also do it checking the properties of different objects or using “reveal” but PAWS works with flags and I thought it would be easier to use them in this particular case.

  • If the approach is not completely stupid, do you know what is going on?

Thank you so much and sorry for the super naive question and the long post, I still did not finish reading the documentation, so it could be that I am missing something super obvious.

Cheers,

Alberto

5 Likes

Welcome to the community! :grin:

It might useful to know if you are using the Adv3 library or Adv3Lite library, because we have different users here who specialize in one or the other.

3 Likes

Hello! :smiley:

I am using adv3.h, so the Adv3 library :slight_smile:

1 Like

… or both :wink:

Best regards from Italy,
dott. Piergiorgio.

3 Likes

Hey Alberto, welcome to TADS!!

I suspect, without testing, I can guess why the battery isn’t disappearing. notifyInsert is a routine that gets called in conjunction with normal putIn processing. Which means that you could interrupt the whole process with failCheck, but if you don’t, the indirect object will move the “put” object into itself after the notifyInsert routine is over. So you are actually zapping the battery, but then the container is immediately recalling it from the void in the action phase. (If I remember right.) To alter that you may need to modify the iobjFor(PutIn) part instead.

You also probably don’t want to define your custom notifyInsert method with nuclear_Battery as a parameter. That typically should be something generic like obj to avoid confusion, since this routine is supposed to be agnostic to what arguments it’s given.

I haven’t used the plural vocab a lot (following the asterisks) but I think you only want a single asterisk to divide between the nouns and plurals; slashes only go between multiple nouns. '(battery) holder/receptacle/snap*holders receptacles snaps' etc.
It’s also just as easy to do 'holder/holders/snap/snaps'’ etc., unless you have more than one object in scope with the same vocabulary that you also want to verb as a batch (if there were multiple holders in a room, and you wanted each one to get its own paragraph if a player typed X HOLDERS).

A more standard TADS approach, instead of assigning a new value to the message, would be

cannotOpenMsg = '<<if batteryInserted>>Leave it alone! <<else>>The normal msg. '

It might not matter, but I’d put Fixture farther right in the class list than Hidden, just because Hidden overrides canBeSensed, and to be safe you want to make sure Fixture’s version isn’t preferred.

As for the flags: I don’t know how many flags you want to add to the game, but I’d put the batteryInserted property right on the holder object.
If you’re going to use an abstract flag object, I would make it an object not a Thing. I, however, simply

modify libGlobal
    myGlobalProp = val

if I need global flags.
Also, if the battery can only go in once and never come out, you don’t really need an integer property either, you could use true/nil or use gRevealed. gRevealed is loosely supposed to mean “does the PC know this fact?” but in practice, it is an extremely handy way to mark anything in the course of the game as having happened, especially whether a block of text has appeared or not.

You said the container wasn’t open after your call… I’m writing all this off the cuff without testing code, so I can’t really say for sure about that yet. Maybe see if this gets you anywhere, and if you still have trouble closing the container, let me know. Good luck Alberto!

5 Likes

Not sure how helpful an ‘I agree’ post is, but JZ made two points that inform my approach to TADS (if not OO coding in general).

One of the philosophies of Object Oriented programming (of which TADS is mostly) is to encapsulate functionality and properties in relevant objects. TADS is EXPLICITLY modelling physical objects, so its more literal than most here! I think you’ll find you fight the language less if you

if (battery.isIn(holder)) { ...

instead of

if (flag_index.battery_inserted == 0) { ...

especially as the game grows and flag_index becomes unruly. You could always override makeOpen() to prevent access to the battery after first inserted, and just leave it in there.

I def agree with both of these statements too, especially for flags that don’t map easily to specific game objects. I would add that if you do

modify libGlobal
     myGlobalProp = val

a semi-standard convention would be to further

#define gMyGlobalProp (libGlobal.myGlobalProp)

so you can

if (gMyGlobalProp) { ...

Though you’ll need to put the #define in a common, included header to ensure all files understand it.

Welcome to TADS, its awesome in here!

5 Likes

Agree wholeheartedly with this:

Slightly different take on this:

Rather than add flags to the library, I tend to create my own global object:

gameState: object
  myGlobalVal = nil

  playerChoseBluePill() {
    // ...
  }
;

But technically either will work.

5 Likes

I would also overwhelmingly prefer
if(battery.isIn(holder))
I simply thought you were saying you had a motive for not doing it that way…

4 Likes

Yet another “more TADS-ish” approach:

desc = "<<if battery.isIn(self)>>The battery's locked in the compartment. 
          <<else>>The battery holder is empty. "

Or by template:

holder: 'holder' 'holder'
        "<<if battery.isIn(self)>>Msg. <<else>>Other msg. "
;

I have split desc into a method as well, but there’s usually a much more complex reason than what’s going on here.

4 Likes

I’ll also make a plug for gRevealed here. I’ve made close to 700 <.reveal someTag>s in my source (which I lazily modified to <.r someTag>), and have made about 1200 checks sourcewide for whether a tag has been gRevealed. The thing is, if you commit to it, you don’t have to define a corresponding property anywhere else, whether on the same object or a globals object. If you’re dealing strictly with code, and you want to note something that has happened and will never be undone, you can call
gReveal('someEvent');
but more often you can just throw the tag in a string:
"With horrifying finality, the event occurs! <.reveal someEvent>";
Note that case is discriminated within tags.

To overwhelm you further :slight_smile: I also created an unreveal tag (which I lazily abbreviated to <.ur someTag>, where the reveal system can now be used like a boolean/switchable property, but again, the property doesn’t need to be defined anywhere (well, it becomes an entry in a table but you don’t have to manage that), and it can be very handy to control things like this from printable strings. (At least, I found situations in my game that made this a pretty awesome boon.)
This prodded me to further make a timed reveal, which also makes absolutely brainless some Fuse-like functionality for things that no ordinary person would take the time to set up a cumbersome Fuse for.

I realize I’m just rambling now, probably to an empty room, but if anybody’s interested I’d be happy to discuss details. I can guarantee you I implemented things in my (so far, only) game that would never have occurred without a streamlined means to do so…

(If you really want the reveal system to be specific to PC game world knowledge, you can make a duplicate of the reveal table, create a different tag and make some tweaks to conversationManager’s customTags and doCustomTag.)

4 Likes

Oh wow! Thank you for all the help guys! I will be slowly going through all the answers to digest them properly :).

The main reason I was thinking to use flags in a single object instead of the much more correct approach of encapsulating properties in the relevant objects is that the original game, being an Amstrad adventure, was designed around “global” flags and variables. In PAWS the game code is mostly a database that is read by an interpreter in everything turn from up to down. All the conditions in the database are evaluated in the same order they appear and, at the end of the turn, the relevant variables and flags are updated. Of course, there are no containers, NPCs, or anything like that. You can implement all those things but you need to come with ways of doing that by hand. You have to stick to tons of limitations, but I find that quite rewarding in a way :).

In this particular case the player enters a derelict space station with all the main systems out of power. When you insert the battery in the holder, all the computers and blast doors power up, so the player can interact with them. Back in the days I simply flagged that the battery was inserted and most of the relevant other entries in the database (the ones corresponding to doors, computers, etc.) were only checking for this condition to work as intended. I wanted to follow a nicer approach, with a properly simulated station containing reactive objects (for example, things would turn on and off depending on the battery being in the generator) but this was not possible given the super small amount of RAM. It would be completely OK in the PC port, but I would not really like to make it substantially “better” than the Amstrad game. I thought more about a straightforward port and then, in case there is a second part in PC, I could try to go crazy with it.

That said, this does not mean that I should not try to make things right in the PC version, so I will see if I come up with easy ways to encapsulate properties in the relevant objects and make it work.

Thanks again! I am sure I will come back with more questions once I have read all the answers in detail! By the way, the original game is totally free, so you can play it if you want. It is quite easy, so don´t expect a big challenge. You will need an Amstrad emulator, though.

3 Likes

Thank you again for all the answers!

I still need to implement the plurals to declare the object names and the other advice you gave me, but at the moment what I did to solve the problem without using flags was something like this:


power_plant_generator_battery_holder: OpenableContainer, SingleContainer, RestrictedContainer, Hidden, Fixture
    vocabWords = '(battery) (generator) holder*holders/receptacle*receptacles/snap*snaps/enclosure*enclosures'
    name = 'battery holder'
    location = Power_plant
    initiallyOpen = true
    validContents = [nuclear_Battery]
    desc()
    {
        if(nuclear_Battery.isIn(power_plant_generator_battery_holder))
        {
            "The battery rests inside. ";
        }
        else
        {
            "The battery holder is open and empty. These generators are plug and play, inserting the right battery on it should power it on. ";
        }
    }
    
    notifyInsert(nuclear_battery, newCont)
    {
        "When I insert the nuclear battery in the holder I hear a click and the compartment closes. ";
        power_plant_generator_battery_holder.makeOpen(nil);
    }
    
    dobjFor(Open)
    {
        check()
        {
            if(nuclear_Battery.isIn(power_plant_generator_battery_holder))
            {
                "The generator is already on, I better don\'t mess with the battery holder anymore. ";
                exit;
            }
        }
    } 
    
    dobjFor(Close)
    {
        check()
        {
            if(!nuclear_Battery.isIn(power_plant_generator_battery_holder))
            {
                "I should not attempt to close it when there is no battery inside. ";
                exit;
            }
        }
    } 
;

When you introduce the battery in the holder it closes automatically and the Open verb gets overridden so it stays closed. In practice this also hides the battery from the player in a permanent way, so any kind of interaction prompts the parser to say that there is not battery around. Moreover, before inserting the battery the compartment is permanently open, so the player cannot mess with it. Also, now all the info is in the relevant objects instead of flags :blush:

I will see to finish creating all the objects, etc., before starting with the puzzles. This way I will be able to focus on one thing and then another :).

3 Likes

Great!
I’ll throw out a few more words, hopefully only to simplify your life as you go forward.
Particularly with yikes-long names like the battery holder :slight_smile: you’ll want to make use of self and the ability to refer to an object’s own properties without qualifiers. Thus:
if(nuclear_Battery.isIn(self))
and
makeOpen(nil)

Within the check method (of Open, in this case) you might want to get used to using
if(battery.isIn(self)) failCheck('Don\'t mess with it. ');

I would write notifyInsert like this:

notifyInsert(obj,newCont) {
    if(obj==battery) { ... }
    else inherited(obj,newCont);
}

if nothing else, just as a habit to get into so you don’t burn yourself later on.
It is also a good habit to get into to write like this:

check {
       if(battery.isIn(self)) failCheck('msg. ');
       else inherited; 
}

just in case you are dealing with a verb that carries “normal” handling in the check method. Again, it’s better to establish the habits so you don’t burn yourself.

(Possible information overload:) Implementing the holder as a SingleContainer isn't really necessary if only one game object is allowed to go in the container. All SingleContainer does is enforce the objEmpty precondition.
You’ll also want to make sure you have a custom cannotPutInMsg(obj) to clue the player that only the battery is going in the compartment.

I can’t resist the extra touches! If you define

unBattery: PresentLater, Unthing 'battery' 'battery'
      'The battery is locked inside the compartment. '
      location = Power_plant
;

and add a code line at the end of notifyInsert

unBattery.makePresent;

Then the game will look a little more graceful when the player tries to refer to the battery after it’s in the holder.

2 Likes

Great advice! I really, really appreciate it. It also comes at a great time, before I went very far with the code :blush:

I will make sure to implement all the stuff and then I will paste the final code here :). I certainly would like to have as many custom messages as possible to cover most possible player inputs. I tried to do that, to a point, in the Amstrad version, but there was a limit (regarding number of entries in the database, just 256 of each type, and memory) and I would like to fix that in PC. Even if the game is basically the same at the end, it would be great if the parser could understand many more things and respond in the correct way :).

2 Likes

I’ll be eager to see the progress!

1 Like

Just noticed a typo you may or may not have found yet:

power_plant_generator_battery_holder: OpenableContainer, SingleContainer, RestrictedContainer, Hidden, Fixture
    vocabWords = '(battery) (generator) holder*holders/receptacle*receptacles/snap*snaps/enclosure*enclosures'

The plurals syntax is not quite right, you want

vocabWords = 
     adj1 adj2 adj3 singleton1/singleton2/singleton3*plural1 plural2 plural3

or less obtusely

    vocabWords = '(battery) (generator) holder/receptacle/enclosure/snap*holders receptacles snaps enclosures'

Also, cannot recommend highly enough getting comfortable with templates. Will improve your coding QOL immeasurably!

1 Like

Holy crap is unObject an amazing recommendation. Adding to my playbook!

2 Likes

Don’t have my code at the moment but I also made mods to the Unthing class so all you have to do is supply an “unObject” and it will copy the vocab and naming from the sim object.
So you could do

unBattery: Unthing -> battery ‘It’s locked in the compartment. ‘ ;
2 Likes

Thanks! I am in the process of correcting the plural syntax of the quite a few objects I already had around! :smiley: I was also making the code neater with “self” and the other recommendations, although I still don´t have the unBattery there!

Templates are definitely very common in TADS… I still prefer the regular code because I find it way easier to read, but I guess I will have to end adopting them :/.

2 Likes

Actually I feel the need of an “unComponent” in a3lite environment, but I’m looking toward your source for ideas for eventually implementing this “unComponent”…

Best regards from Italy,
dott. Piergiorgio.

2 Likes