Derived but Static Vocabulary

I feel like I have gone a long way around when there should be a simpler solution. To make my life easier, I am looking to programmatically derive positional adjectives.

Let’s say I am programming a pub crawl game. A series of beer joints have a variable number of taps (say 1-5) on their respective bars. Most of them derive from common descriptions. I want each pub to auto-populate ‘first tap’ ‘second tap’ ‘third tap’ etc. as names and vocab. The problem, I think, is that by the time the compiler can evaluate the code, the vocabWords are already baked.

My ‘solution’ was to define a base class with a ‘get my order among peers’ routine, use that routine both for name and additionalVocab which I one-time updated when the parent was first examined. Here’s what that mess looked like:

class BasicTap : Component  // should not be instantiated, use subclasses instead
    vocabWords = 'tap/pull/spout*spouts taps pulls'
    name = '<<getOrder()>> tap'
    getOrder() {
        local lst = [];                 //temp sibling list holder
        local orderDesc = '';    //positional descriptive string to return

        if (location == nil) return '';   // be weird, a tap floating in space

        foreach(local x in location.allContents)  //find all neighbor taps
            if (x.ofKind(BasicTap)) lst += x;
        lst = lst.sort(nil, {a,b: a.pluralOrder - b.pluralOrder} );  // pluralOrder is nice for lists too
        local ord = lst.indexOf(self);    // find position among neighbors

        if (lst.length() == 1) orderDesc = 'single';  // only one
        else
            if (ord == lst.length()) orderDesc = 'last';
                else
                    switch(ord) {
                        case 1:  orderDesc = 'first'; break;
                        case 2:  orderDesc = 'second'; break;
                        case 3:  orderDesc = 'third'; break;
                        case 4:  orderDesc = 'fourth'; break;
                        default:  "ERROR(BasicTap.getOrder()):  too many taps(<<lst.length()>>) defined for <<location.theName>>";
                                break;
                    }
        return orderDesc;
    }
    additionalVocab = ''  // override when subclass instantiated, will be dynamically updated by Bar with initializeVocabWith(str)
    pluralOrder = (sourceTextOrder)  // I was betting this wouldn't work, but it did!  So far...
    // Additional functionality excised...
;
/*
 *  Individual Tap type subclasses
 */
class pilsnerTap : BasicTap
    additionalVocab = '<<getOrder()>> pilsner -'
    desc = "A plain handle, releasing a crisp, golden beer.  "
;
class ipaTap : BasicTap
    additionalVocab = '<<getOrder()>> ipa -'
    desc = "A garish handle, releasing an aromatic, hoppy beer.  "
;
class sourTap : BasicTap
    additionalVocab = '<<getOrder()>> sour -'
    desc = "A faux rustic handle, releasing a tart, cloudy beer.  "
;
class belgianTap : BasicTap
    additionalVocab = '<<getOrder()>> belgian -'
    desc = "An Old World handle, releasing a strong, heavier beer.  "
;
class stoutTap : BasicTap
    additionalVocab = '<<getOrder()>> stout -'
    desc = "A culturally insensitive handle, releasing a dark, chewy beer.  "
;
/*
 *
 *  The class that will instantiate Taps as Components.  Updates tap vocab when first X'd
 *
 */
class Bartop : Platform
    dobjFor(Examine) {
        action() {
            if (!described) {                   // first time only
                foreach(local x in allContents) // expand tap vocab
                    if (x.ofKind(BasicTap)) x.initializeVocabWith(x.additionalVocab);
            }
            inherited;
        }
    }
;

Yeah, that’s a lot of code, just so I can elsewhere do this:

bougieBar : Bartop 'bougie bar' 'bougie bar'
     "A bar trying way too hard.  "
;
+ ipaTap 'rightmost -';
+ ipaTap 'middle -'
    desc = "A plain handle, releasing a stinky, hoppy beer.  "
;
+ ipaTap 'leftmost -'
    desc = "A broken handle, releasing beer indistinguishable from perfume water.  "
;

authenticBar : Bartop 'authentic bar' 'authentic bar'
     "A bar that oozes authenticity.  The neighboring floor is sticky with it.  "
;
+ belgianTap;
+ stoutTap;
+ pilsnerTap;
+ sourTap;

//and so on

Long example, lot of coding*, but it feels worth it. I’m putting down a lot of these things (some taps locally overridden in one way or another), and minimizes my cut’n’paste and edit drudgery of positional adjectives. But it also feels like I crawled up my butt a bit to implement it this way. Is there an easier solution I’m totally blind-spotting?

*This example still has a problem. If the player jumps to >X FIRST TAP before even looking at the bar, they’ll get a deceptive message. That could be addressed by drafting seen, or on entering bar, or something else. Problem is still kludginess of relying on another object to update its vocab.

1 Like

This at least localizes the update to the Tap item itself, the Bartop EXAMINE kludge isn’t needed. Still quite a journey.

modify BasicTap
    noteSeenBy(actor, prop) {
        if ((actor == gPlayerChar) && !seen) initializeVocabWith(additionalVocab);
        inherited(actor, prop);
    }
;
1 Like

I actually did this at the weekend - the bar crawl, not the coding…

Would it be possible to write an initiating function that takes your data and creates each tap object. It could set the position and neighbours at that time. That is how I would do it, but I would not be using TADS, so may not be workable here.

1 Like

Instead of using initializeVocabWith(), try using cmdDict.addWord() to add an adjective to an existing Thing. You can also use a preinit object to walk through all your taps and set them up, including assigning them to bars and tweaking their vocabulary, unless there’s some reason why you need to shuffle them when the player examines them for the first time. You can use cmdDict.removeWord() in addition to cmdDict.addWord() to twiddle the vocabulary at runtime if this is in fact what you need to do, i.e. if the taps change as the game progresses or something.

Here’s a little demo of the two bits I’m talking about:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

tapPreinit: PreinitObject
        execute() {
                forEachInstance(Tap, function(o) {
                        o.initializeTap();
                });
        }
;

class Tap: Component
        _tapPositions = static [ 'first', 'second', 'third' ]
        _tapIndex = nil

        initializeTap() {
                if((location == nil) || !location.ofKind(Bar)) return;
                location.addToBar(self);
        }
        twiddleTapVocab(idx) {
                if((idx < 1) || (idx > _tapPositions.length)) return;
                _tapIndex = idx;
                "INIT:  <<name>> is <<tapPosition()>>\n ";
                cmdDict.addWord(self, tapPosition(), &adjective);
        }
        tapPosition() {
                return(_tapIndex ? _tapPositions[_tapIndex] : 'unknown');
        }
;
class Bar: Platform
        _tapList = static []

        addToBar(obj) {
                if((obj == nil) || !obj.ofKind(Tap)) return;
                _tapList += obj;
                obj.twiddleTapVocab(_tapList.length);
        }
;

startRoom:      Room 'Void' "This is a featureless void with a bar. ";
+me: Person;
+bar: Bar 'bar' 'bar'
        "It's a barely-implemented bar. It has <<spellInt(_tapList.length)>>
        taps. ";
++tapYan: Tap 'yan tap' 'yan tap'
        "It's a yan tap.  It is the <<tapPosition()>> tap. "
;
++tapTan: Tap 'tan tap' 'tan tap'
        "It's a tan tap.  It is the <<tapPosition()>> tap. "
;
++tapTethera: Tap 'tethera tap' 'tethera tap'
        "It's a tethera tap.  It is the <<tapPosition()>> tap. "
;
        

versionInfo:    GameID;
gameMain:       GameMainDef initialPlayerChar = me;

There’s a little bit of chatty init debugging that you wouldn’t want in a real game but might help if you’re compiling with the debugging flag (the init messages will be output at compile time if you’re not).

The example code produces the desired behavior, as I understand it:

INIT: tethera tap is first
INIT: tan tap is second
INIT: yan tap is third
Void
This is a featureless void with a bar.

You see a bar here.

>x first tap
It's a tethera tap.  It is the first tap.

>x second tap
It's a tan tap.  It is the second tap.

>x tan tap
It's a tan tap.  It is the second tap.
4 Likes

PreinitObject! That’s definitely better. I was hoping there would be some undiscovered syntax to get vocabWords right initially (and concisely), but yours is more concise in any case.

So far, this board has been a super-effective debug tool!

Unrelated Q: is there a convention with _symbol v symbol_? I had internalized that underscore was code for ‘internal use’ and prefix was for methods and suffix for properties. I was really just making leaps of logic from code I’d encountered. Ie

class NewClass : Thing
     internalVar_ = nil

     _internalFn(stat) {
          internalVar_ = stat;
     }
;
1 Like

Typographic conventions in programming are a whole…thing…and a lot of people have strong religious convictions surrounding them. So with the disclaimer that I’m just talking about my own personal ideas and I’m not trying to set them down as natural laws or anything:

  • Class names are in upper CamelCase (first letter capitalized)
  • Variable, property, and method names are in lower camelCase (lower case first letter)
  • Preprocessor flags are in SCREAMING_SNAKE_CASE (all caps, words separated by an underscore)
  • Property and method names with a leading underscore are meant to be local to their containing objects. T3 doesn’t have any concept of a protected variable, so the naming convention is purely advisory. Basically the idea is that pebble.foozle is meant to be accessible by external callers, while pebble._foozle isn’t. It’s worth noting here that nothing in adv3 does this, about which see below.
  • Property and method names with a trailing underscore are system internals. Basically a “removing this voids the warranty” seal. I avoid using this, except where I’m twiddling something from the system library, entirely to call out the difference between “private” stuff in my code and “private” stuff from the system.
  • Preprocessor flags with a leading double underscore are debugging only. There’s only one example of this in adv3, and it’s the __DEBUG flag.

I don’t think T3 actually has any stated conventions for any of this, but the above is (afaik) mostly compatible with the library coding’s conventions. I do use different intent style than adv3; adv3 prefers BSD-style conventions for whitespace and braces, while I almost always use K&R for C-ish languages. So where T3 would write:

if(foo == bar)
{
     method();
}

…I’d prefer…

if(foo == bar) {
     method();
}

Like I said, this is all stuff that some people have very strong options about. My personal feeling is that you should just pick the style that makes it easiest for you to understand/read your own code and stick to it and not worry about much else. It’s a truism in gamedev that, to a first order approximation, nobody cares what your code looks like. So do what makes it easiest for you to internalize/work with.

3 Likes

Random addendum that I thought of while working on something else:

T3 has a spellIntOrdinal() method that makes it unnecessary to use an array of counting words, the way I did in my example. You can declare the class for the taps using something like:

class Tap: Component
        _tapIndex = nil

        desc = "It's <<aName>>.  It is the <<tapPosition()>> tap. "

        initializeTap() {
                if((location == nil) || !location.ofKind(Bar)) return;
                location.addToBar(self);
        }
        twiddleTapVocab(idx) {
                if((dataType(idx) != TypeInt) || (idx < 1)) return;
                _tapIndex = idx;
                cmdDict.addWord(self, tapPosition(), &adjective);
        }
        tapPosition() {
                return(_tapIndex ? spellIntOrdinal(_tapIndex) : 'unknown');
        }
;

…instead. Which isn’t a big deal in this example but is convenient a) if you have to count a large number of widgets, and b) if you’re worried about localization.

1 Like

I believe internal class, object, function and method names have an underscore prefix, while in properties it’s a suffix.