Dynamic Topic addition?

I would like to add a list of randomized AskTopic/SuggestedTopic\s to a base class (Person). Like, a random person might like to talk about cheese, money… Another about the sun and the flat earth, etc.

Does anyone know how to do this? I already have the randomized list of Topics down. Now I need to add them.

i don’t have time to try it out myself, but i’d assume the inherited methods from TopicDatabase would do the trick. probably best to do this in a constructor. here’s some example code for using one.
let me know if it works out (and of course if it doesn’t :slight_smile: ).

1 Like

I tried some stuff… and all I did was break the player. I don’t know how I did that considering this is i an entirely different file. Now when I move or even POW I get a nil object reference.

cheese : AskTopic, SuggestedAskTopic, StopEventList @cheese
    ['I like cheese']
    name = 'cheese'
    timesToSuggest = 3
    construct(location_, lst?){
        location = location_;
        inherited(lst);
    }
;

honey : AskTopic, SuggestedAskTopic, StopEventList @honey
    ['I like honey']
    name = 'honey'
    timesToSuggest = 3
    construct(location_, lst?){
        location = location_;
        inherited(lst);
    }
;

joney : AskTopic, SuggestedAskTopic, StopEventList @joney
    ['I like joney']
    name = 'joney'
    timesToSuggest = 3
    construct(location_, lst?){
        location = location_;
        inherited(lst);
    }
;

trees : AskTopic, SuggestedAskTopic, StopEventList @trees
    ['I like trees']
    name = 'trees'
    timesToSuggest = 3
    construct(location_, lst?){
        location = location_;
        inherited(lst);
    }
;

patrick : AskTopic, SuggestedAskTopic, StopEventList @patrick
    ['I like patrick']
    name = 'patrick'
    timesToSuggest = 3
    construct(location_, lst?){
        location = location_;
        inherited(lst);
    }
;

sun : AskTopic, SuggestedAskTopic, StopEventList @sun
    ['I like the sun']
    name = 'the sun'
    timesToSuggest = 3
    construct(location_, lst?){
        location = location_;
        inherited(lst);
    }
;

possibleInterests : ShuffledList
    [
        function(loc_) { new cheese(loc_);},
        function(loc_) { new honey(loc_);},
        function(loc_) { new joney(loc_);},
        function(loc_) { new trees(loc_);},
        function(loc_) { new patrick(loc_);},
        function(loc_) { new sun(loc_);}
    ]
;

// This is our Generic character, otherwise, Basic Tenant.
class GenericCharacter : Person, InitObject
    properName = nil
    makeProper() // This proper definition helps with proper output. (Turning 'he/she...' into 'Bob/Bobette...')
    { 
        if(properName != nil) // Make sure we have a name.
        { 
            name = properName; // Make our name the proper name.
            initializeVocabWith(properName); // We need to be able to reference this name.
            isProperName = true; // to make sure {the bob/he} works as expected.
        } 
        return name; // For compactness.
    }
    curState = defaultAskState
    ownedRoom = nil // The room they own
    // No need to initalize names and such -- Those are defined per-object.
    
    numInterests = 3 // The number amount of interests a person can have. 
                     // Must be less than the # of possibleInterests you have,
    interests : ShuffledList { }

    execute() // Our random interests
    {
        local intr = []; // local list
        local x = nil, y = nil;
        for(local a = 0; a <= (numInterests - 1); a++) { // loop for the amount of interests
            x = possibleInterests.getNextValue();
            y = x(self);
            intr = intr.append(y); // Add to the list
            defaultAskState.addSuggestedTopic(y);
        }
        interests.construct(intr); // Put it in our shuffled list, so we get that shuffled behavior.
    }
    
    
;
defaultAskState : ActorState {
    autoSuggest = true
    construct(loc_){
        location = loc_;
    }
} // Default State for most things.

bob : GenericCharacter
    vocabWords = 'guy'
    location = plazaWestSide
    name = 'guy'
    properName = 'Bob'
    ownedRoom = basicRoom1
    desc = "Hello. My name is <<makeProper()>>. <<toString(interests.getNextValue())>>."
;

^
There’s the monstrosity I’ve created, if you’d like to tell me what I did that was stupid. Please. Because I sure don’t understand.

Hey there… I, too, don’t have time at the moment to do a full test, but one thing caught my eye… (not a programmer outside of TADS, so I’m just used to seeing TADS syntax)
I was wondering if the interests property should start as nil, and the final statement in character.execute should say interests = new ShuffledList(intr);? May help nothing at all… another bit that may be only babbling: you have possibleInterests as a ShuffledList filled with functions, would that be better done with ShuffledEventList since doScript() automatically calls elements that are functions? But that may make no difference either. I feel like looking at your situation there is a more TADS-esque way of approaching it, but my head’s pretty tired right now. If you don’t get it solved before then, I can try to play with it tomorrow maybe…

Hi again, I did some quick and dirty experiments, and I think I found a few things. In short, I can get the game to do
x bob
I’m Bob. I like the sun
g
I’m Bob. I like honey
g
I’m Bob. I like trees
g
I’m Bob.I like honey
etc…

A couple of things: I think your possibleInterests functions need to return a new object, rather than just stating “new cheese…”
Then the desc property of the generic characters needs to call the topicResponse property of getNextValue, because the getNextValue value is a TopicEntry object, not a string. Actually, with your StopEventList template notation, for some reason I was getting nilObjRefs when I tried to call doScript. So I didn’t take time to solve that, though it doesn’t seem like it would be too complicated, rather, I changed [‘I like the sun’] to topicResponse = "I like the sun." etc.
Also used initializeActor and took InitObject off of the class list, and used addTopic instead of constructors. Seems to me like the defaultAskState should be a class if there’s multiple characters, I changed that. See what you think of this (unrefined and hastily thrown-together) code:

possibleInterests : ShuffledList
    [
        function(loc_) { return new cheese2(loc_);},
        function(loc_) { return new honey2(loc_);},
        function(loc_) { return new joney(loc_);},
        function(loc_) { return new trees(loc_);},
        function(loc_) { return new patrick(loc_);},
        function(loc_) { return new sun(loc_);}
    ]
;

class GenericCharacter : Person
    desc = "Hello. My name is <<name>>. <<interests.getNextValue().topicResponse()>>."
    curState = perInstance(new DefaultAskState(self))
    numInterests = 3 // The number amount of interests a person can have. 
    interests = nil
    initializeActor
    	{inherited();
        local intr = []; // local list
        local x = nil, y = nil;
        for(local a = 1; a <= numInterests; a++) { // loop for the amount of interests
            x = possibleInterests.getNextValue();
	    y = (x)(self);
            addTopic(y);
            intr += y; // Add to the list
            curState.addSuggestedTopic(y);
        }
        interests = new ShuffledList(intr);     }   
;

DefaultAskState : ActorState 
    autoSuggest = true
;

bob : GenericCharacter 'bob' 'Bob'
    location = plazaWestSide
    desc = "Hello. My name is Bob. <<interests.getNextValue().topicResponse()>>."
;

Also, not sure if these interests are only meant to be part of the examine descriptions, but if you want them to be actual asktopics, there are no vocabwords defined for your objects, and they’re kind of circularly referenced. This might need to be substantially altered if these are going to be ask about topics…
Also didn’t really troubleshoot the ActorState part of this, at the moment was just going for the desc property and the shuffled list behavior; pretty sure it’s not right. But just a note, you shouldn’t have to override the ActorState constructor since it already takes one argument that sets the location…

Thank you, you’re a lifesaver. I’m new to TADS and am still climbing the learning curve to it (coming from JS and Python) so this is new to me. These are only templates, as the characters with random interests are tenants in a hotel, and aren’t important. I only want the player to feel like each tenant is different in their own way, them being interested in different stuff. But yes, this is perfect. I’ll try this and see how I’ll fare.

Welcome to TADS! As mentioned, I had no programming background whatsoever before TADS, so it’s kind of all I know, but even then, I’m no guru. I did eventually learn just enough Python to solve some problems on the Project Euler website. Can’t always guarantee how much free time I’ll have, but don’t hesitate to shoot out a question if you’re stuck on something!

I’m not getting anywhere, really. But maybe there’s a simpler way to do what I want…

What I want is, when someone is in a room, (maybe when it;s just you and a tenant) or maybe one of the random greetings, for them to voice their interests. That way, my NPC tenants feel dynamic. But, I also want to handle if the player actually asks about their interests, as well. Is what I’m doing here much to complicated for my desired result?

Edit: As my comment in the file says…

/*
 *  Actor Code -- I want to define my actors here.
 *  The hotel will be filled with people, with random names, but concrete roles for each room. 
 *  Each Basic Room Member will:
 *    (1) Respond to investigating rooms and such.
 *    (2) Have a dummy-list of Topics of interest. Random, so it feels life-like.
 *    (3) Walk around randomly, (but just stay on their floor. Too complicated to simulate the elevator.)
 *  Each Complex Room Member (Every TDS and Master Suite Owners #1 & #4) will:
 *    (1) Do many of the same things Basic Room Members do, albeit no dummy-list.
 *    (2) Might refuse access to their room.
 *    (3) Handle special case conversations.
 *  Special-Case Handlers include non-room tenants. They'll handle conversations, 
 *  but not hold them, and stay reserved to their post. This makes them easy to find, always.
 *  
 *  The OTHER exception to this is the manager, who leaves when her purpose is over, for family matters.
 *  If you have the manager there, all the problems are just solved.
 *
 *  I'll also include my Hint system, in the form of a very helpful dog. :)
 *
 
 */

Sorry, are you asking if the overview comment sounds like more work than it’s worth, or are you asking if it’s too complicated to implement? I definitely wouldn’t say it’s too complicated to do, if you know how you want it to act.
Could you make the interests Topic objects (as opposed to TopicEntrys), where an AskTopic could reference them, but the NPC could also talk about them on their own through AgendaItems or InitiateTopics, or else add a text property to each Topic object, which a HelloTopic would call embedded in the main greeting?

Also, if you’re still trying to figure out how to make “autonomous” NPCs, something I did was to read the source code for Emily Boegheim’s game It. That helped open my eyes to what you can do with AgendaItems, scripted travels, modifying Actor/Person library behavior, and thoroughly-implemented NPCs in general.

1 Like

Okay I was gone for a few days but I worked on it recently and have this:

class Interest : Topic;

cheese : Interest
    vocabWords = 'cheese'
;

honey : Interest
    vocabWords = 'honey'
;

money : Interest
    vocabWords = 'money'
;

trees : Interest
    vocabWords = 'trees'
;

patrick : Interest
    vocabWords = 'patrick'
;

sun : Interest
    vocabWords = 'sun'
;

possibleInterests : ShuffledList
    [
        cheese,
        honey,
        money,
        trees,
        patrick,
        sun
    ]
;

class InterestTopic : AskTellTopic
    construct(loc_, match_, resp_) {
        self.location = loc_;
        self.matchObj = match_;
        self.topicResponse = resp_;
    }
;

// This is our Generic character, otherwise, Basic Tenant.
class GenericCharacter : Person, InitObject
    properName = nil
    makeProper() // This proper definition helps with proper output. (Turning 'he/she...' into 'Bob/Bobette...')
    { 
        if(properName != nil) // Make sure we have a name.
        { 
            name = properName; // Make our name the proper name.
            initializeVocabWith(properName); // We need to be able to reference this name.
            isProperName = true; // to make sure {the bob/he} works as expected.
        } 
        return name; // For compactness.
    }
    curState = perInstance(new DefaultAskState(self, self))
    ownedRoom = nil // The room they own
    // No need to initalize names and such -- Those are defined per-object.
    
    numInterests = 3 // The number amount of interests a person can have. 
                     // Must be less than the # of possibleInterests you have,
    interests = []
    

    initializeActor // Our random interests
    {
        inherited();
        local intr = []; // local list
        local x = nil, top = nil;
        for(local a = 0; a <= numInterests; a++) { // loop for the amount of interests
            x = possibleInterests.getNextValue();
            
            top = new InterestTopic(self, x, {: "hi"});
            
            addTopic(top);
            
            intr += x; // Add to the list
        }
        interests += intr; // Put it in our shuffled list, so we get that shuffled behavior.
    }
    
    
;
class DefaultAskState : InConversationState {
    autoSuggest = true
    construct(actor, loc_) { 
        inherited(actor);
        location = loc_;
    }
    
} // Default State for most things.

bob : GenericCharacter
    vocabWords = 'guy'
    location = plazaWestSide
    name = 'guy'
    properName = 'Bob'
    ownedRoom = basicRoom1
    desc = "Hello. My name is <<makeProper()>>."
;

When you ASK BOB ABOUT MONEY it says ‘Nothing obvious happens’. If you ask about something that isn’t his interest, he responds rightly with ‘Bob doesn’t respond.’ Why doesn’t the topicResponse respond?

Hey, been busy. I’ll see if I can find time to take another look this evening…

Hi… here’s a few things to note:

class InterestTopic : AskTellTopic
    construct(loc) { location = loc; }
;
// took out the other two parameters

    initializeActor // Our random interests
    {
        inherited();
        local intr = []; 
        local x, top;
        for(local a = 1; a <= numInterests; a++) { 
//TADS starts its indexes with 1, not 0, so if you want just 3 interests, you need to set 'a' to 1
            x = possibleInterests.getNextValue();
            top = new InterestTopic(self);  // giving the TopicEntry a location
            top.matchObj = x;   // set matchObj now
	    top.setMethod(&topicResponse, 'This is Bob\'s answer');
// this is the method you'll need to use to change a property that may have code in it. However, 
// you'll have to figure out how to correlate the string arguments to fill up the new topicResponse. 
// It's seeming to me like you should just create static InterestTopics with all this info declared 
//in your source code rather than in this dynamic initializer. Then initializeActor() just becomes a
// matter of randomly selecting from topics that are defined elsewhere...
            addTopic(top);     
            intr += x; 
        }
        interests = intr;    //I'm assuming you were still going to use this statement in part of the 
// characters' descriptions? 
    }  
;

Oohh. Thanks. I didn’t know lists start at 1. And I was not aware of setMethod. But, I can use static, global topics? I didn’t know that. I thought, from reading the Tour guide, they had to be inside of the ActorState.

You should be able to define the TopicEntrys outside the actor. Your initializeActor routine is adding that Entry to your actor’s particular topicDatabase. I think multiple actors could reference the same topicEntry object (you could give it a source code name instead of making them anonymous, if that helped anything)

Actually, check that… you may be able to make the standalone TopicEntrys a class. Then initializeActor can do addTopic(new SunInterestTopic(self));, or whatever…

Yay! I made it work like this:


class Interest : Topic;

cheese : Interest 'cheese'
;

honey : Interest 'honey'
;

money : Interest 'money'
;

trees : Interest 'trees'
;

patrick : Interest 'patrick'
;

sun : Interest 'sun'
;

class CheeseTopic : AskTellTopic 
    topicResponse = "I like cheese."
    matchObj = cheese
;

class HoneyTopic : AskTellTopic 
    topicResponse = "I like honey."
    matchObj = honey
;

class MoneyTopic : AskTellTopic 
    topicResponse = "I like money."
    matchObj = money
;

class TreesTopic : AskTellTopic 
    topicResponse = "I like trees."
    matchObj = trees
;

class PatrickTopic : AskTellTopic 
    topicResponse = "I like patrick."
    matchObj = patrick
;

class SunTopic : AskTellTopic 
    topicResponse = "I like the sun."
    matchObj = sun
;


// This is our Generic character, otherwise, Basic Tenant.
class GenericCharacter : Person, InitObject
    properName = nil
    makeProper() // This proper definition helps with proper output. (Turning 'he/she...' into 'Bob/Bobette...')
    { 
        if(properName != nil) // Make sure we have a name.
        { 
            name = properName; // Make our name the proper name.
            initializeVocabWith(properName); // We need to be able to reference this name.
            isProperName = true; // to make sure {the bob/he} works as expected.
        } 
        return name; // For compactness.
    }
    ownedRoom = nil // The room they own
    // No need to initalize names and such -- Those are defined per-object.
    
    numInterests = 3 // The number amount of interests a person can have. 
                     // Must be less than the # of possibleInterests you have,
    interests = []
    

    initializeActor // Our random interests
    {
        inherited();
        local intr = []; // local list
        local x = [
            function(){ return perInstance(new CheeseTopic()); }, 
            function(){ return perInstance(new HoneyTopic()); }, 
            function(){ return perInstance(new MoneyTopic()); }, 
            function(){ return perInstance(new TreesTopic()); }, 
            function(){ return perInstance(new PatrickTopic()); }, 
            function(){ return perInstance(new SunTopic()); }
            ], y = 0, top = nil;
        for(local a = 0; a <= numInterests; a++) { // loop for the amount of interests
            y = rand(x.length); // store the random value
            top = x[y](); // Grab our random topic
            x = x.removeElementAt(y); // Remove said topic from list
            top.location = self; // Set our location.
            
            
            intr += top; // Add to the list
        }
        interests += intr; // Put it in our property
    }
    
    linkInterestsIn(state_) {
        foreach ( local x in self.interests ) {
            state_.addTopic(x);
            state_.addSuggestedTopic(x);
        }
    }
    
    
;

bob : GenericCharacter
    vocabWords = 'guy'
    location = plazaWestSide
    name = 'guy'
    properName = 'Bob'
    ownedRoom = basicRoom1
    desc = "Hello. My name is <<makeProper()>>."
    initializeActor 
    {
        inherited();
        linkInterestsIn(bobTalking);
    }
;

+ bobTalking : InConversationState
    specialDesc = "He's waiting for you to respond."
    stateDesc = "{The bob/ he}'s waiting for you to respond."
    attentionSpan = 5
;

++ bobReady : ConversationReadyState 
    isInitState = true
    autoSuggest = true
    specialDesc = "He's standing around, examining the plaza."
    stateDesc = "He's standing idly."
;

This also allows me to link the interests in wherever I want! And I can still use the list for small talk! Thanks for your help!

All right, great! Good luck! I didn’t test this, but just by glancing at your new code, I think that local x = [ function(){ return perInstance(new CheeseTopic()); }, function(){ return perInstance(new HoneyTopic()); }, function(){ return perInstance(new MoneyTopic()); }, function(){ return perInstance(new TreesTopic()); }, function(){ return perInstance(new PatrickTopic()); }, function(){ return perInstance(new SunTopic()); } ] could be as simple as

local x = [new CheeseTopic(), new SunTopic() ... ]
At the least, I doubt if you need the perInstance in there since this is within a code block… I think the perInstance macro is more for defining a property like someProperty = perInstance(new Object())
Have fun!