/* asktell.t: ASK/TELL-based conversation system for TADS * V1.0 * Suzanne Skinner, 1999, Public Domain * tril@igs.net * * This file implements a topic-based ask/tell system similar to that used * used by WorldClass. It also implements information sources (i.e. things * that let you look up information), since they are a simple and obvious * addition. It requires TADS 2.4 at minimum (since it uses 2.4's new * disambiguation hook, disambigXobj). Preferably, you should use a patched * or later version of TADS, since 2.4 has a glitch which will cause * disambiguation questions to look odd. * * To use asktell.t, include it after adv.t in your main source file. See * the comments in the source below, especially for the superclasses topic, * ioTopicVerb, and movableActor, to learn how to implement conversations * using this library. * * This file uses #pragma C+, but sets back to #pragma C- at the end. * * Features: * * + Full disambiguation: If you "ask so-and-so about tree", and there * are two different trees you might be asking about, the game will * respond with a disambiguation question: just as with other verbs. * * + Topic-based: Indirect objects for ASK/TELL are scoped to allow * topics only. Non-topic objects will never show up in disambiguation * questions. * * + Knowledge-based: A topic may be known or unknown at a given time. * Unknown topics will also never show up in disambiguation questions. * * + Mimesis-preserving: If the player uses vocabulary that doesn't match * any topics, the npc's "disavow" property will be output, instead of * a cryptic message such as "something tells you that won't be a very * productive topic". disambigXobj allows us to accomplish this. * * Tips: * * + Be careful of unknown topics. If you have a topic that can be * referred to by a very general noun, e.g. "enchanted tree" (which * can be called simply "tree" by the player), it may be best to make * it a knownTopic. Or, make sure there is a general known topic that * matches that noun from the start (e.g. a "forest" * object). Otherwise, the player may get peeved by the seemingly * erroneous "you don't know about that" message. * * + WorldClass sets up carryable items to be topics by default. I * recommend against this, at least in a large game (in my own game, * there are often 3-4 different versions of the same item floating * around). Keep the topic system separate to help insure that no * impossible disambiguation questions will show up ("Which ball do * you mean, the ball, the ball, or the ball?"). * * + This library assumes that only topics draw a distinction between * known and unknown. But if you prefer a WCish world in which * *anything* can be unknown (and therefore, "ask about [unknown * non-topic]" should result in "you don't know about that"), the * changes shouldn't be hard to make. Just fiddle with disambigIobj. * * + Scenario: You have in your game a red gem and a green sword. But * nowhere does there exist a green gem. What happens when the player * types "ask so-and-so about green gem"? This: * * I don't see any green gem here. * * This happens in any TADS implementation of ASK/TELL. It is the * catchall error message displayed when a non-existent noun/adjective * combination is used. This is an artifact of how TADS is set up * internally: it favors local-scope verbs, for which that error *does* * make sense. You can change the message (#9) using parseError to say * something more meaningful (e.g. "I don't know of any such thing"), * but there are complications: the same error message is used for * other situations of a different nature. Also, there is no practical * way (that I know of) to determine what verb was used in this case. * It is handled too early in the parsing process. * * It's possible to fix this somewhat, but tricky. I won't go into * details here, but if enough people ask, i can add the solution into * this source file. */ #pragma C+ /*************************** New Superclasses ***************************/ // topic: something which can be asked about, told about, or looked up in // information sources. The code for scoping and disambiguation with topics // is largely contained in ioTopicVerb. // // Important properties: // + known: indicates whether the player currently knows about this // topic. If nil, it will never show up in disambiguation questions, // and the player cannot get information by asking about it. // + unknownMsg: the message printed when the player's vocabulary matches // only unknown topic(s). class topic: thing known = nil unknownMsg = "You don't know about that." location = nil ; // knownTopic: a topic which is known from the beginning. // // Important properties: none class knownTopic: topic known = true ; // infoSource: something the player can look things up in. // // Important properties: // + askTopics(topic, words): This works the same as askTopics in // movableActor. "topic" is the topic asked about, and "words" are the // vocabulary words that were used to refer to it. The method should // output text and return true if the infoSource has an entry for that // topic, otherwise return nil. This method will never be called with // an unknown topic. // + disavow: This method will be printed whenever askTopics returns nil. class infoSource: thing askTopics(topic, words) = {return nil;} disavow = "There's no entry for that topic." verIoLookupIn(actor) = { if (self.location != Me) "You're not holding <>."; } ioLookupIn(actor, dobj) = { // We have to handle catchallUnknownTopic here, unlike with other // verbs: if (dobj == catchallUnknownTopic) topic.unknownMsg; else if (!self.askTopics(dobj, objwords(1))) self.disavow; } verDoConsultOn(actor, io) = {self.verIoLookupIn(actor);} doConsultOn(actor, io) = { if (!self.askTopics(io, objwords(2))) self.disavow; } ; // ioTopicVerb: a verb which uses topics as indirect objects. Non-topics // will never show up in disambiguation questions. validIo and validIoList // are not used for scoping, nor are verIoAskAbout and the like used. // Instead, all disambiguation is done within disambigIobj. If no topics // match the player's input, that method will return the special catchall // topic catchallNonTopic. Since no NPC's or infoSources know about this // topic, it will simply cause a "disavow" to be printed. Similarly, if // topics match but no *known* topics match, catchallUnknownTopic will be // returned, which will cause a reply of "you don't know about that" to // any type of query (ask, tell, consult, look up). // // Important properties: none class ioTopicVerb: deepverb validIoList(actor, prep, dobj) = (nil) validIo(actor, obj, seqno) = true ioDefault(actor, prep) = (nil) disambigIobj(actor, prep, dobj, verprop, wordlist, objlist, flaglist, numberWanted, isAmbiguous, silent) = { local i, len; local newlist = []; local unknownTopicsFound = nil; len = length(objlist); for (i=1; i <= len; i++) { if (isclass(objlist[i], topic)) { if (objlist[i].known) newlist += objlist[i]; else unknownTopicsFound = true; } } if (length(newlist) < 1) { if (unknownTopicsFound) newlist += catchallUnknownTopic; else newlist += catchallNonTopic; } return newlist; } ; // doTopicVerb: a verb which uses topics as direct objects. disambigDobj // simply calls disambigIobj on ioTopicVerb. See ioTopicVerb for more // details. // // Important properties: none class doTopicVerb: deepverb validDoList(actor, prep, io) = (nil) validDo(actor, obj, seqno) = true doDefault(actor, prep, io) = (nil) // Allowing multiple objects can cause erroneous sdescs // (e.g. catchallNonTopic.sdesc) to get printed. rejectMultiDobj(prep) = { "You can't use multiple objects with that verb."; return true; } disambigDobj(actor, prep, io, verprop, wordlist, objlist, flaglist, numberWanted, isAmbiguous, silent) = { return ioTopicVerb.disambigIobj(actor, prep, io, verprop, wordlist, objlist, flaglist, numberWanted, isAmbiguous, silent); } ; /************************** adv.t Modifications **************************/ // Modifications to thing for default responses to ask, tell, consult, // look up, and redirections of ioConsultOn to doConsultOn, ioAskFor to // doAskFor. // // Important properties: none, unless you want to change default responses. modify thing replace verDoAskAbout(actor, io) = {"There is no response.";} verIoAskFor(actor) = {} verDoAskFor(actor, io) = {"There is no response.";} ioAskFor(actor, dobj) = {dobj.doAskFor(actor, self);} replace verDoTellAbout(actor, io)= {"There is no response.";} verIoConsultOn(actor) = {} verDoConsultOn(actor, io) = {"That's not an information source.";} ioConsultOn(actor, dobj) = {dobj.doConsultOn(actor, self);} verIoLookupIn(actor) = {"That's not an information source.";} verDoLookupIn(actor, io) = {} ; // Modifications to movableActor (parent class for all actors) // // Important properties: // + askTopics(topic, words): This method should output text and return // true for a valid topic, otherwise return nil, in which case disavow // will be printed by doAskAbout. The specific vocabulary words used to // refer to the topic are passed in the "words" parameter. // + tellTopics(topic, words): works almost identically to askTopics. // + doAskFor(actor, io): You should override this method if you want to // allow the player to ask this NPC *for* something. Like "ask about", // it takes topics only. By default, it simply outputs self.disavow. // + disavow: default changed to "There is no response.". This method will // be called if askTopics returns nil. // + tellDisavow: calls disavow by default. This method will be called if // tellTopics returns nil. modify movableActor askTopics(topic, words) = {return nil;} tellTopics(topic, words) = {return nil;} doAskFor(actor, io) = {self.disavow;} replace disavow = "There is no response." tellDisavow = {self.disavow;} replace doAskAbout(actor, io) = { if (!self.askTopics(io, objwords(2))) self.disavow; } verDoTellAbout(actor, io) = {} doTellAbout(actor, io) = { if (!self.tellTopics(io, objwords(2))) self.tellDisavow; } verDoAskFor(actor, io) = {} ; /**************************** Special Objects ****************************/ // catchallNonTopic: If the game is expecting a topic, and the player // enters vocabulary that does not match any objects of the topic class // (known or unknown), then the disambiguation function on ioTopicVerb or // doTopicVerb will return a single-item list consisting of // catchallNonTopic. Since no NPC or infoSource will have a response/entry // for this topic, it will simply cause a "disavow" statement to be // printed. // // Important properties: none catchallNonTopic: knownTopic sdesc = "non-topic (you should never see this)" ; // catchallUnknownTopic: Similar to catchallNonTopic, this topic will be // returned by disambiguation functions if the player's vocabulary matches // no *known* topics, but at least one unknowntopic. Default messages are // set here so that ask, tell, and consult will all output topic.unknownMsg // in response. The default message for "look up" must be handled in // infoSource itself, in ioLookupIn. // // Important properties: none catchallUnknownTopic: knownTopic sdesc = "unknown topic (you should never see this)" ioAskAbout(actor, dobj) = {topic.unknownMsg;} ioAskFor(actor, dobj) = {topic.unknownMsg;} ioTellAbout(actor, dobj) = {topic.unknownMsg;} ioConsultOn(actor, dobj) = {topic.unknownMsg;} ; /*************************** New Prepositions ***************************/ forPrep: Prep preposition = 'for' sdesc = "for" ; /************************ New and Replaced Verbs ************************/ replace askVerb: ioTopicVerb, darkVerb verb = 'ask' sdesc = "ask" prepDefault = aboutPrep ioAction(aboutPrep) = 'AskAbout' ioAction(forPrep) = 'AskFor' ; replace tellVerb: ioTopicVerb, darkVerb verb = 'tell' sdesc = "tell" prepDefault = aboutPrep ioAction(aboutPrep) = 'TellAbout' ; consultVerb: ioTopicVerb verb = 'consult' sdesc = "consult" prepDefault = onPrep ioAction(onPrep) = 'ConsultOn' ioAction(aboutPrep) = 'ConsultOn' ; lookupVerb: doTopicVerb verb = 'look up' 'read about' sdesc = "look up" prepDefault = inPrep ioAction(inPrep) = 'LookupIn' ; #pragma C-