Thank you all for the input. At first I’ve tried to make a proxy version of ConversationReadyState and InConversationState objects under both actors to redirect conversation to shared objects of the same class, but the mechanism was quite complex and I’ve quicky lost a track and didn’t produce anything useable.
But on the second try I’ve realized (quite suprisingly) that I can change the state of the second character during conversation with the first one to his InConversationState and nothing bad happens, the player can talk to both of them without any conflict. From there it was much easier, I’ve just nedded to automatically sync states of both characters, sync their boredom counter and make a proxy not for whole conversation state but only for topic database and here is what I came with:
[code]
#charset “utf-8”
#include <adv3.h>
#include <en_us.h>
versionInfo: GameID;
gameMain: GameMainDef
initialPlayerChar = me
;
room: Room ‘room’ ‘room’
"Room. "
vocabWords = 'room'
;
/******************************************************************************/
/*
- DisambigDeferrer by Michael J. Roberts from Return to Ditch Day
- “Disambiguation deferrer” is a mix-in class that can be combined with
- any ordinary Thing class to create an object that defers in
- disambiguation situations to a given enumerated set of objects, or to
- objects that match some rule. If we find any object from our
- enumerated set, or any object that matches our rule, we’ll take
- ourselves out of the picture for disambiguation purposes.
- Deferral is useful in a lot of cases. Frequently, one object will
- have a much stronger affinity for a given set of vocabulary words than
- most other objects, but we’ll nonetheless want to make the other
- objects match the vocabulary when the stronger object isn’t around.
- The deferrer lets the weaker objects opt out when they see the
- stronger object as a possible match.
/
class DisambigDeferrer: object
/
- A list of objects I defer to. We’ll remove ourselves from parsing
- consideration if any of these objects are in scope.
*/
disambigDeferTo =
/*
* Do we defer to the given object? Returns true if we defer to the
* given object, nil if not. By default, we defer if the object
* appears in our disambigDeferTo list.
*/
disambigDeferToObj(obj) { return disambigDeferTo.indexOf(obj) != nil; }
/*
* Filter the noun-phrase resolution list. If we find any objects
* that we defer to in the match list, we'll defer by removing 'self'
* from the match list.
*/
filterResolveList(lst, action, whichObj, np, requiredNum)
{
/* if there's an object we defer to, defer to it */
if (lst.indexWhich({x: disambigDeferToObj(x.obj_)}) != nil)
{
/* find myself in the list */
local i = lst.indexWhich({x: x.obj_ == self});
/* remove myself from the list */
lst = lst.removeElementAt(i);
}
/* return the updated list */
return lst;
}
;
/*
- ConversationStateSync is a mix-in class to be added to each of the actors
- we need to keep in sync during conversation. When the class observes that
- its actor changes from ConversationReadyState to InConversationState and
- back, it will do the same change on the other actor so both will pretend
- to be talking at the same time.
- We do not pay an attention to conversation state changes other than the
- ones specified lower. Naturally we need to add this class to both actors
- and interlink them bidirectionaly together.
/
class ConversationStateSync: object
/ Who is the other actor? */
otherActor = nil
/* My conversation states we observe. */
myConversationReadyState = nil
myInConversationState = nil
/* Other actor's conversation states to which we synchronise. */
otherConversationReadyState = nil
otherInConversationState = nil
/* Observe the state changes and do the same change on other actor. */
setCurState(state)
{
inherited(state);
if(curState == myConversationReadyState && otherActor.curState
!= otherConversationReadyState)
otherActor.setCurState(otherConversationReadyState);
if(curState == myInConversationState && otherActor.curState
!= otherInConversationState)
otherActor.setCurState(otherInConversationState);
}
/*
* Also we must keep in sync the boredomCounter to prevent one actor to
* leave conversation.
*/
noteConvAction(other)
{
inherited(other);
if(curState == myInConversationState && otherActor.boredomCount > 0)
otherActor.noteConvAction(other);
}
;
/*
- We need a place to store common set of conversation topics for both
- actors. CommonTopicDatabase is a class meant to be a root of conversation
- topics. It inherits from ActorTopicDatabase the same way as Actor,
- ActorState or ConvNode. We just override a method distinguishing to whom
- we are actually talking (ProxyTopicDatabase notifies us about this).
*/
class CommonTopicDatabase: ActorTopicDatabase
fromActor = nil
getTopicOwner() { return fromActor; }
;
/*
- This class is to be added to InConversationState of both actors and
- manages proxying of topic searching from the actor’s topic database to
- common topic database of both actors. Or more precisely it mixes both
- topic databases together so you can have some topics common for both
- actors while on the same time have some topics unique for each actor.
/
class ProxyTopicDatabase: object
/ CommonTopicDatabase object storing topics. */
proxyTarget = nil
/*
* Get this state's suggested topic list. ConversationReady states
* shouldn't normally have topic entries of their own, since a
* ConvversationReady state usually forwards conversation handling
* to its corresponding in-conversation state. So, simply return
* the suggestion list from our in-conversation state object.
*/
stateSuggestedTopics()
{
local lst = [];
if(suggestedTopics != nil)
lst += suggestedTopics;
if(proxyTarget.suggestedTopics != nil)
lst += proxyTarget.suggestedTopics;
return lst.length() ? lst : nil;
}
/*
* This method is taken from the library, only modifications are
* topicList retrieval and filling proxyTarget.fromActor.
*
* find the best response (a TopicEntry object) for the given topic
* (a ResolvedTopic object)
*/
findTopicResponse(fromActor, topic, convType, path)
{
local topicList;
local best, bestScore;
proxyTarget.fromActor = self.location;
/*
* Get the list of possible topics for this conversation type.
* The topic list is contained in one of our properties; exactly
* which property is determined by the conversation type.
*/
topicList = (self.(convType.topicListProp) != nil ?
self.(convType.topicListProp) : [])
+ (proxyTarget.(convType.topicListProp) != nil ?
proxyTarget.(convType.topicListProp) : []);
/* if the topic list is nil, we obviously won't find the topic */
if (topicList.length() == 0)
return nil;
/* scan our topic list for the best match(es) */
best = new Vector();
bestScore = nil;
foreach (local cur in topicList)
{
/* get this item's score */
local score = cur.adjustScore(cur.matchTopic(fromActor, topic));
/*
* If this item has a score at all, and the topic entry is
* marked as active, and it's best (or only) score so far,
* note it. Ignore topics marked as not active, since
* they're in the topic database only provisionally.
*/
if (score != nil
&& cur.checkIsActive()
&& (bestScore == nil || score >= bestScore))
{
/* clear the vector if we've found a better score */
if (bestScore != nil && score > bestScore)
best = new Vector();
/* add this match to the list of ties for this score */
best.append(cur);
/* note the new best score */
bestScore = score;
}
}
/*
* If the best-match list is empty, we have no matches. If
* there's just one match, we have a winner. If we found more
* than one match tied for first place, we need to pick one
* winner.
*/
if (best.length() == 0)
{
/* no matches at all */
best = nil;
}
else if (best.length() == 1)
{
/* exactly one match - it's easy to pick the winner */
best = best[1];
}
else
{
/*
* We have multiple topics tied for first place. Run through
* the topic list and ask each topic to propose the winner.
*/
local toks = topic.topicProd.getOrigTokenList().mapAll(
{x: getTokVal(x)});
local winner = nil;
foreach (local t in best)
{
/* ask this topic what it thinks the winner should be */
winner = t.breakTopicTie(best, topic, fromActor, toks);
/* if the topic had an opinion, we can stop searching */
if (winner != nil)
break;
}
/*
* If no one had an opinion, run through the list again and
* try to pick by vocabulary match strength. This is only
* possible when all of the topics are associated with
* simulation objects; if any topics have pattern matches, we
* can't use this method.
*/
if (winner == nil)
{
local rWinner = nil;
foreach (local t in best)
{
/* get this topic's match object(s) */
local m = t.matchObj;
if (m == nil)
{
/*
* there's no match object - it's not comparable
* to others in terms of match strength, so we
* can't use this method to break the tie
*/
winner = nil;
break;
}
/*
* If it's a list, search for an element with a
* ResolveInfo entry in the topic match, using the
* strongest match if we find more than one.
* Otherwise, just use the strength of this match.
*/
local ri;
if (m.ofKind(Collection))
{
/* search for a ResolveInfo object */
foreach (local mm in m)
{
/* get this topic */
local riCur = topic.getResolveInfo(mm);
/* if this is the best match so far, keep it */
if (compareVocabMatch(riCur, ri) > 0)
ri = riCur;
}
}
else
{
/* get the ResolveInfo object */
ri = topic.getResolveInfo(m);
}
/*
* if we didn't find a match, we can't use this
* method to break the tie
*/
if (ri == nil)
{
winner = nil;
break;
}
/*
* if this is the best match so far, elect it as the
* tentative winner
*/
if (compareVocabMatch(ri, rWinner) > 0)
{
rWinner = ri;
winner = t;
}
}
}
/*
* if there's a tie-breaking winner, use it; otherwise just
* arbitrarily pick the first item in the list of ties
*/
best = (winner != nil ? winner : best[1]);
}
/*
* If there's a hierarchical search path, AND this topic entry
* defines a deferToEntry() method, look for matches in the
* inferior databases on the path and check to see if we want to
* defer to one of them.
*/
if (best != nil && path != nil && best.propDefined(&deferToEntry))
{
/* look for a match in each inferior database */
for (local i = 1, local len = path.length() ; i <= len ; ++i)
{
local inf;
/*
* Look up an entry in this inferior database. Pass in
* the remainder of the path, so that the inferior
* database can consider further deferral to its own
* inferior databases.
*/
inf = path[i].findTopicResponse(fromActor, topic, convType,
path.sublist(i + 1));
/*
* if we found an entry in this inferior database, and
* our entry defers to the inferior entry, then ignore
* the match in our own database
*/
if (inf != nil && best.deferToEntry(inf))
return nil;
}
}
/* return the best matching response object, if any */
return best;
}
;
/*****************************************************************************/
/
- Example usage mainly based on some conversation examples from Eric Eve’s
- book Learning TADS 3.
*/
alice: ConversationStateSync, Actor ‘alice’ ‘Alice’ @room
/* Sync with other actor. */
otherActor = bob
myConversationReadyState = aliceLooking
myInConversationState = aliceTalking
otherConversationReadyState = bobLooking
otherInConversationState = bobTalking
;
-
aliceTalking: ProxyTopicDatabase, InConversationState
specialDesc = "Alice is standing by the shop window, waiting for you to
speak. "
stateDesc = "She’s waiting for you to speak. "
/* Where are the commont topics? */
proxyTarget = commonTopics
;
++ aliceLooking: ConversationReadyState
isInitState = true
commonDesc = " standing in the street, peering into a shop window. "
specialDesc = “Alice is <>”
stateDesc = “She’s <>”
;
+++ HelloTopic, StopEventList
[
'Hello, there! you say.\b
Hi! Alice replies, turning to you with a smile. ',
'Hello, again, you greet her.\b
Yes? she replies, turning back to you. ’
]
;
+++ ByeTopic
"Well, cheerio then! you say.\b
'Bye for now, Alice replies, turning back to the shop window. "
;
+++ BoredByeTopic
"Alice gives up waiting for you to speak and turns back to the shop window. "
;
+++ LeaveByeTopic
"Alice watches you walk away, then turns back to the shop window. ";
;
++ AskTopic @alice
"How are you today? you ask.\b
Fine, just fine, she assures you. "
;
/******************************************************************************/
bob: DisambigDeferrer, ConversationStateSync, Actor ‘bob’ ‘Bob’ @room
/* Sync with other actor. */
otherActor = alice
myConversationReadyState = bobLooking
myInConversationState = bobTalking
otherConversationReadyState = aliceLooking
otherInConversationState = aliceTalking
/*
* We don't want push the player into deciding to which of the NPC he
* wants to talk so we made on of the NPCs to step back while in
* disambiguation situation.
*/
disambigDeferToObj(obj) { return obj == alice && me.isIn(room); }
;
-
bobTalking: ProxyTopicDatabase, InConversationState
specialDesc = "Bob is standing by the shop window, waiting for you to
speak. "
stateDesc = "He’s waiting for you to speak. "
/* Where are the commont topics? */
proxyTarget = commonTopics
;
++ bobLooking: ConversationReadyState
isInitState = true
commonDesc = " standing in the street, peering into a shop window. "
specialDesc = “Bob is <>”
stateDesc = “He’s <>”
;
+++ HelloTopic, StopEventList
[
'Hello, there! you say.\b
Hi! Bob replies, turning to you with a smile. ',
'Hello, again, you greet him.\b
Yes? he replies, turning back to you. ’
]
;
+++ ByeTopic
"Well, cheerio then! you say.\b
'Bye for now, Bob replies, turning back to the shop window. "
;
+++ BoredByeTopic
"Bob gives up waiting for you to speak and turns back to the shop window. "
;
+++ LeaveByeTopic
"Bob watches you walk away, then turns back to the shop window. ";
;
++ AskTopic @bob
"How are you today? you ask.\b
Fine, just fine, he assures you. "
;
/******************************************************************************/
- commonTopics: CommonTopicDatabase;
++ AskTopic @room
"What do you thing about this room? you ask.\b
Pretty bare, but fine, he assures you. "
;
++ DefaultAnyTopic
/* Sometimes we need to know to whom we are talking to adjust common messages. */
"We can discuss that when I’m not so busy, <<location.fromActor.name>> suggests. "
;[/code]
And although some minor glitches, such as bye message seems to come only from Bob, it seems to work overall, although still only basically tested:
[code]
Room
Room.
Alice is standing in the street, peering into a shop window.
Bob is standing in the street, peering into a shop window.
x bob
He’s standing in the street, peering into a shop window.
x alice
She’s standing in the street, peering into a shop window.
talk to alice
“Hello, there!” you say.
“Hi!” Alice replies, turning to you with a smile.
x bob
He’s waiting for you to speak.
a alice
“How are you today?” you ask.
“Fine, just fine,” she assures you.
ask bob about room
“What do you thing about this room?” you ask.
“Pretty bare, but fine,” he assures you.[/code]