# CFDialog.py - Dialog helper class # # Copyright (C) 2007 Yann Chachkoff # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # The author can be reached via e-mail at lauwenmark@gmail.com # # What is this about ? #======================= # This is a small set of utility classes, to help you creating # complex dialogs. It is made for those who do not want to # bother about complex programming, but just want to make a few # dialogs that are better than the @match system used in the # server. # # How to use this. #======================= # First, you need to import DialogRule and Dialog classes. Add the # following line at the beginning of your script: # from CFDialog import DialogRule, Dialog # Then, you can go to the dialogs themselves. A dialog is made of # several rules. Each rule is made of keywords, preconditions, # postconditions, answers and pre/postfunction. # - Keywords are what the rule will be an answer to. For example, if # you want a rule to be triggered when the player will say "hi", # then "hi" is the keyword to use. You can associate more than a # keyword to a rule by concatenating them with the "|" character. # Finally, "*" is a special keyword that means: "match everything". # "*" is useful to provide generic answers. # - Answers are what will be said when the rule is triggered. This is # what the NPC replies to the player. Answers are stored in a list. # When there is more than one answer in that list, one will be # selected at random. # - Preconditions are flags that must match for the rule to be triggered. # Each precondition has a name and a value. The default value of a # precondition is "0". The flags are stored into each player, and will # survive between gaming sessions. They are useful to set the point in a # dialog reached by a player - you can see those as an "NPC memory". All # conditions must have a name and a value. The ":" and ";" characters are # forbidden. For a rule to be triggered, all the player's flags should match # the preconditions. A precondition is a list in the form: [key, value]. # All preconditions are stored in a list. Each "value" may be a single # string or a multiple choice string where the options are concatenated # together with the "|" character. When a value is set to "*", a match is # not required to trigger the rule. # - Postconditions are state changes to apply to the player's conditions # after the rule has been triggered. The postcondition is a list in the # form: [key, value] and the format is similar to preconditions except that # the "|" has no special meaning in the value and is forbidden. A value of # "*" means that the condition will not be touched. # - A prefunction is an optional callback function that will be called # when a rule's preconditions are all matched, but before the rule is # validated. # The callback can do additional tests, and should return 1 to allow the rule # to be selected, 0 to block the rule. The function arguments are the player # and the actual rule being tested. # - A postfunction is an optional callback that is called when a rule has # been applied, and after the message is said. It can do additional custom # processing. The function arguments are the player and the actual rule # having been used. # # Once you have defined your rules, you have to assemble them into a dialog. # Each dialog involves somebody who triggers it, somebody who answers, and # has a unique name so it cannot be confused with other dialogs. # Typically, the "one who triggers" will be the player, and the "one who # answers" is an NPC the player was taking to. You are free to chose whatever # you want for the dialog name, as long as it contains no space or special # characters, and is not used by another dialog. # You can then add the rules you created to the dialog. Rules are parsed in # a given order, so you must add the most generic answer last. # # Like the @match system, CFDialog converts both match strings and the things # the player says to lowercase before checking for a match. # # A simple example #================= # I want to create a dialog for an old man. If I say "hello" or "hi" for the # first time, grandpa will greet me. If I say it for the second time, he'll # grumble (because he's like that, you know :)). I also need a generic answer # if I say whatever else. # In this example, the player is stored in 'player', and the old man in 'grandpa'. # What the player said is in 'message'. # ## Dialog creation: # speech = Dialog(player, grandpa, "test_grandpa_01") # ## The first rule is the "hello" answer, so we place it at index 0 of the ## rules list. The precondition is that we never said hello before. ## The postcondition is to mark "hello" as "1", to remember we already ## greeted grandpa. # prer = [["hello","0"]] # postr = [["hello", "1"]] # rmsg = ["Hello, lad!","Hi, young fellow!","Howdy!"] # speech.addRule(DialogRule("hello|hi", prer, rmsg, postr),0) # ## The second rule is the answer to an hello if we already said it before. ## Notice that we used "*" for the postcondition value, meaning that we ## are leaving it as it is. # prer = [["hello","1"]] # postr = [["hello", "*"]] # rmsg = ["I've heard, you know, I'm not deaf *grmbl*"] # speech.addRule(DialogRule("hello|hi", prer, rmsg, postr),1) # ## And finally, the generic answer. This is the last rule of the list. ## We don't need to match any condition, and we don't need to change them, ## so we use "*" in both cases this time. # prer = [["hello","*"]] # postr = [["hello", "*"]] # rmsg = ["What ?", "Huh ?", "What do you want ?"] # speech.addRule(DialogRule("*", prer, rmsg, postr),2) # # A more complex example # ====================== # ../scorn/kar/gork.msg is an example that uses "|" in the precondition. Note # how this lets the conversation fork and merge. Once Gork tells us he has a # friend who lives in a tower, the player can fork the conversation to either # a friend or a tower thread. Without the "|" in the precondition, it would # be harder to construct the conversation in a way that either fork merges # back into the main thread, while also allowing the player the player to # "remember" something Gork said previously and still use it in the original # context. Note also how easy it is to allow the conversation to loop about # mid-thread. # import Crossfire import string import random class DialogRule: def __init__(self,keyword,presemaphores, message, postsemaphores, prefunction = None, postfunction = None): self.__keyword = keyword self.__presems = presemaphores self.__message = message self.__postsems= postsemaphores self.__prefunction = prefunction self.__postfunction = postfunction def getKeyword(self): return self.__keyword def getMessage(self): msg = self.__message l = len(msg) r = random.randint(0,l-1) return msg[r] def getPreconditions(self): return self.__presems def getPostconditions(self): return self.__postsems def getPrefunction(self): return self.__prefunction def getPostfunction(self): return self.__postfunction class Dialog: def __init__(self,character,speaker,location): self.__character = character self.__location = location self.__speaker = speaker self.__rules = [] def addRule(self, rule, index): self.__rules.insert(index,rule) def speak(self, msg): for rule in self.__rules: if self.isAnswer(msg, rule.getKeyword())==1: if self.matchConditions(rule)==1: self.__speaker.Say(rule.getMessage()) self.setConditions(rule) return 0 return 1 def isAnswer(self,msg, keyword): if keyword=="*": return 1 keys=string.split(keyword,"|") for ckey in keys: if string.find(msg.lower(),ckey.lower())!=-1: return 1 return 0 def matchConditions(self,rule): for condition in rule.getPreconditions(): status = self.getStatus(condition[0]) values=string.split(condition[1],"|") for value in values: if (status==value) or (value=="*"): break else: return 0 if rule.getPrefunction() <> None: return rule.getPrefunction()(self.__character, rule) return 1 def setConditions(self,rule): for condition in rule.getPostconditions(): key = condition[0] val = condition[1] if val!="*": self.setStatus(key,val) if rule.getPostfunction() <> None: rule.getPostfunction()(self.__character, rule) def getStatus(self,key): character_status=self.__character.ReadKey("dialog_"+self.__location); if character_status=="": return "0" pairs=string.split(character_status,";") for i in pairs: subpair=string.split(i,":") if subpair[0]==key: return subpair[1] return "0" def setStatus(self,key, value): character_status=self.__character.ReadKey("dialog_"+self.__location); finished="" ishere=0 if character_status!="": pairs=string.split(character_status,";") for i in pairs: subpair=string.split(i,":") if subpair[0]==key: subpair[1]=value ishere=1 if finished!="": finished=finished+";" finished=finished+subpair[0]+":"+subpair[1] if ishere==0: if finished!="": finished=finished+";" finished=finished+key+":"+value self.__character.WriteKey("dialog_"+self.__location, finished, 1)