/* asktell.t: ASK/TELL-based conversation system for TADS
* V1.0
* Suzanne Skinner, 1999, Public Domain
*
[email protected]
*
* 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 <<self.thedesc>>.";
}
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-