Changeset 668 for trunk/game/scripts


Ignore:
Timestamp:
11/17/10 21:44:05 (9 years ago)
Author:
technomage
Message:

Ticket #269: Patch by Technomage.

  • Working prototype of the redesigned DialogueEngine? for the upcoming Techdemo2 release; major redesign of the existing DialogueEngine? and YAML dialogue file syntax.
  • Moved the redesigned DialogueEngine? class to the more descriptive dialogueengine.py module; the DialogueEngine? is now a singleton object and provides all functionality through class methods and attributes, and thus should not be instantiated
  • Abstracted the data structures used to store dialogue data away from the YAML data structures; the relevant classes are stored in the dialogue.py module
  • Abstracted the dialogue commands/actions from the DialogueEngine? code to make modifications and maintenance of dialogue logic easier; the relevant classes are stored in the dialogueactions.py module
  • The PyYAML loader has been replaced with a more robust YamlDialogueParser? class (see the dialogueparsers.py module) that interfaces with the new dialogue data structure classes and supports the new YAML dialogue file syntax; an OldYamlDialogueParser? class is provided to support reading the old Techdemo1 syntax
  • Removed the existing dialogue validator; runtime dialogue validation is not yet implemented
  • Added the convert_dialogue script, which converts dialogue files in the old Techdemo1 format to the new format; all existing dialogue files have been converted to work with the new parser
  • Added two support modules for the new classes and script: ordereddict.py, which provides a Python 2.7-like OrderedDict? class for Python versions 2.4-2.6; optionparser.py, which is a simplified command-line option parser for writing scripts as an alternative to argparse and optparse
  • Updated the dialogue_demo.py script to work with the new DialogueEngine?
  • fixes[s:trac, t:269]
Location:
trunk
Files:
4 added
5 edited
1 moved

Legend:

Unmodified
Added
Removed
  • trunk

    • Property svn:ignore
      •  

        old new  
        44.settings 
        55Thumbs.db 
         6 
         7.externalToolBuilders 
         8 
         9.cproject 
  • trunk/game/scripts/__init__.py

    r147 r668  
     1#   This file is part of PARPG. 
     2 
     3#   PARPG is free software: you can redistribute it and/or modify 
     4#   it under the terms of the GNU General Public License as published by 
     5#   the Free Software Foundation, either version 3 of the License, or 
     6#   (at your option) any later version. 
     7 
     8#   PARPG is distributed in the hope that it will be useful, 
     9#   but WITHOUT ANY WARRANTY; without even the implied warranty of 
     10#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
     11#   GNU General Public License for more details. 
     12 
     13#   You should have received a copy of the GNU General Public License 
     14#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>. 
     15 
     16COPYRIGHT_HEADER = """\ 
     17#   This file is part of PARPG. 
     18 
     19#   PARPG is free software: you can redistribute it and/or modify 
     20#   it under the terms of the GNU General Public License as published by 
     21#   the Free Software Foundation, either version 3 of the License, or 
     22#   (at your option) any later version. 
     23 
     24#   PARPG is distributed in the hope that it will be useful, 
     25#   but WITHOUT ANY WARRANTY; without even the implied warranty of 
     26#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
     27#   GNU General Public License for more details. 
     28 
     29#   You should have received a copy of the GNU General Public License 
     30#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>. 
     31""" 
  • trunk/game/scripts/dialogue.py

    r591 r668  
    1515#   You should have received a copy of the GNU General Public License 
    1616#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>. 
     17try: 
     18    from collections import OrderedDict 
     19except ImportError: 
     20    # Python version 2.4-2.6 doesn't have the OrderedDict 
     21    from scripts.common.ordereddict import OrderedDict 
    1722 
    18 import logging 
    19 import itertools 
     23class Dialogue(object): 
     24    __slots__ = ['npc_name', 'avatar_path', 'start_section_id', 'sections'] 
     25     
     26    def __init__(self, npc_name, avatar_path, start_section_id, 
     27                 dialogue_sections=None): 
     28        self.npc_name = npc_name 
     29        self.avatar_path = avatar_path 
     30        self.start_section_id = start_section_id 
     31        self.sections = OrderedDict() 
     32        if (dialogue_sections is not None): 
     33            for section in dialogue_sections: 
     34                self.sections[section.id] = section 
     35     
     36    def __repr__(self): 
     37        string_representation = ( 
     38            ('Dialogue(npc_id={0.npc_name}, avatar_path={0.avatar_path}, ' 
     39             'start_section_id={0.start_section_id}, ...)').format(self) 
     40        ) 
     41        return string_representation 
    2042 
    21 class EndException(Exception): 
    22     """EndException is used to bail out from a deeply nested 
    23        runSection/continueWithResponse call stack and end the 
    24        conversation""" 
    25     pass 
    2643 
    27 class ResponseException(Exception): 
    28     """ResponseException is used to bail out from a deeply nested 
    29        runSection/continueWithResponse call stack and allow the user to 
    30        specify a response""" 
    31     pass 
     44class DialogueNode(object): 
     45    """Abstract base class that represents a node or related group of 
     46       attributes within a Dialogue.""" 
     47    def __init__(self, text, actions=None): 
     48        self.text = text 
     49        self.actions = actions or [] 
    3250 
    33 class BackException(Exception): 
    34     """BackException is used to bail out from a deeply nested 
    35        runSection/continueWithResponse call stack and rewind the section 
    36        stack""" 
    37     pass 
    3851 
    39 class DialogueEngine(object): 
    40     def __init__(self, obj, callbacks = None, state = None): 
    41         """A very simple dialogue engine for a game. 
    42            d = DialogueEngine(tree, callbacks) 
    43            d = DialogueEngine('screenplay.yaml', callbacks)""" 
    44         if isinstance(obj, dict): 
    45             self.tree = obj 
    46         elif isinstance(obj, str): 
    47             import yaml 
    48             self.tree = yaml.load(file(obj)) 
     52class DialogueSection(DialogueNode): 
     53    """DialogueNode that represents a distinct section of the dialogue.""" 
     54    __slots__ = ['id', 'text', 'actions', 'responses'] 
     55     
     56    def __init__(self, id, text, responses=None, actions=None): 
     57        DialogueNode.__init__(self, text=text, actions=actions) 
     58        self.id = id 
     59        if (responses is not None): 
     60            self.responses = list(responses) 
    4961 
    50         logging.basicConfig(level=logging.INFO) 
    5162 
    52         self.callbacks = callbacks or {} 
    53         self.state = state or {} 
    54         self.section_stack = [] 
    55  
    56     def run(self): 
    57         """Start running the dialogue engine. 
    58            @returns: list of lists (if requesting a response) 
    59            @returns: None (if at the end of the script)""" 
    60         start_section = self.tree['START'] 
    61  
    62         npc_name_cb = self.callbacks.get('npc_name') 
    63         if npc_name_cb: 
    64             npc_name_cb(self.tree['NPC']) 
    65              
    66         npc_avatar_cb = self.callbacks.get('npc_avatar') 
    67         if npc_avatar_cb: 
    68             npc_avatar_cb(self.tree['AVATAR']) 
    69  
    70         try: 
    71             self.runSection(start_section) 
    72         except EndException: 
    73             # we stopped talking to the NPC 
    74             logging.debug("Reached the end") 
    75             end_cb = self.callbacks.get('end') 
    76             if end_cb: 
    77                 end_cb() 
    78             return 
    79         except ResponseException, e: 
    80             return e.args[0] 
    81         except BackException, e: 
    82             self.section_stack.pop(-1) 
    83             try: 
    84                 self.runSection(self.section_stack[-1]) 
    85                 return e 
    86             except ResponseException, e: 
    87                 return e.args[0] 
    88  
    89     def getSection(self, section_name): 
    90         """Return a section object. 
    91            @type section_name: string 
    92            @param section_name: The section to get 
    93            @return: dict""" 
    94         return self.tree['SECTIONS'][section_name] 
    95  
    96     def reply(self, response): 
    97         """After being prompted to provide a response, reply is called to 
    98            submit a response. 
    99            @type choice: int 
    100            @param choice: the index of the response to submit 
    101            @return: list of lists (if requesting a response) 
    102            @return: None (if at the end of the script)""" 
    103         while True: 
    104             try: 
    105                 if response is not None: 
    106                     self.continueWithResponse(self.section_stack[-1], \ 
    107                                                 response) 
    108                 else: 
    109                     self.runSection(self.section_stack[-1]) 
    110             except ResponseException, e: 
    111                 logging.debug("Got response exception %s" , e.args ) 
    112                 return e.args[0] 
    113             except BackException, e: 
    114                 # e.args contains the section to jump back to 
    115                 if e.args: 
    116                     stack = self.section_stack[:] 
    117                     stack.reverse() 
    118                     for i, s in enumerate(stack): 
    119                         if s == e.args[0]: 
    120                             # remove the end of the section stack up to desired 
    121                             # section 
    122                             del self.section_stack[-i:] 
    123                             break 
    124                 else: 
    125                     self.section_stack.pop(-1) 
    126                 response = None 
    127                 continue 
    128             except EndException: 
    129                 end_cb = self.callbacks.get('end') 
    130                 if end_cb: 
    131                     end_cb() 
    132                 logging.debug("Reached the end") 
    133                 return 
    134  
    135     def continueWithResponse(self, section_name, response): 
    136         """Reply to a response in a section and continue executing dialogue 
    137            script 
    138            @type section_name: str 
    139            @param section_name: the section to continue 
    140            @type response: int 
    141            @param response: the index [0,n-1] of the desired response 
    142            @raises: EndException on end of script 
    143            @raises: BackException on "back" reply 
    144            @return: None""" 
    145         state = self.state 
    146         if len(self.section_stack) > 1: 
    147             if self.section_stack[-1] == self.section_stack[-2]: 
    148                 self.section_stack.pop(-1) 
    149  
    150         for command in itertools.cycle(self.getSection(section_name)): 
    151             if not command.get('responses'): 
    152                 continue 
    153  
    154             responses = [] 
    155             for r in command.get('responses'): 
    156                 cond = r[2:] 
    157                 try: 
    158                     if not cond or eval(cond[0], state, {}): 
    159                         responses.append(r) 
    160                 except Exception: 
    161                     print "Error in response conditional: %s" % (cond[0],) 
    162  
    163             section = responses[response][1] 
    164             logging.debug("User chose %s", (section, )) 
    165  
    166             if section == "back": 
    167                 raise BackException() 
    168             elif section.startswith("back "): 
    169                 raise BackException(section[5:]) 
    170             elif section == "end": 
    171                 raise EndException() 
    172  
    173             self.runSection(section) 
    174  
    175     def runSection(self, section_name): 
    176         """Run a section 
    177            @type section_name: string 
    178            @param section_name: The section to run 
    179            @return: None 
    180            @raises: EndException on end of script 
    181            @raises: BackException on "back" reply""" 
    182  
    183         state = self.state 
    184         self.section_stack.append(section_name) 
    185  
    186         if len(self.section_stack) > 1: 
    187             if self.section_stack[-1] == self.section_stack[-2]: 
    188                 self.section_stack.pop(-1) 
    189  
    190         logging.debug("In runSection %s %s", (section_name, \ 
    191                                                self.section_stack,)) 
    192         for command in itertools.cycle(self.getSection(section_name)): 
    193             logging.debug("command was %s", (command,)) 
    194             if command.get("say"): 
    195                 if self.callbacks.get('say'): 
    196                     self.callbacks["say"](state, command["say"]) 
    197  
    198             elif command.get("responses"): 
    199                 responses = [] 
    200                 for response in command.get('responses'): 
    201                     cond = response[2:] 
    202                     if not cond or eval(cond[0], state, {}): 
    203                         responses.append(response) 
    204                 if self.callbacks.get("responses"): 
    205                     self.callbacks["responses"](state, responses) 
    206  
    207                 raise ResponseException(responses) 
    208  
    209             elif command.get("start_quest"): 
    210                 self.callbacks["start_quest"](state, 
    211                         command.get("start_quest")) 
    212              
    213             elif command.get("restart_quest"): 
    214                 self.callbacks["restart_quest"](state, 
    215                         command.get("restart_quest")) 
    216              
    217             elif command.get("complete_quest"): 
    218                 self.callbacks["complete_quest"](state, 
    219                         command.get("complete_quest")) 
    220  
    221             elif command.get("fail_quest"): 
    222                 self.callbacks["fail_quest"](state, 
    223                         command.get("fail_quest")) 
    224  
    225             elif command.get("increase_value"): 
    226                 self.callbacks["increase_value"](state, 
    227                         command.get("increase_value")["quest"], 
    228                         command.get("increase_value")["variable"], 
    229                         command.get("increase_value")["value"]) 
    230  
    231             elif command.get("decrease_value"): 
    232                 self.callbacks["decrease_value"](state, 
    233                         command.get("decrease_value")["quest"], 
    234                         command.get("decrease_value")["variable"], 
    235                         command.get("decrease_value")["value"]) 
    236  
    237             elif command.get("set_value"): 
    238                 self.callbacks["set_value"](state,  
    239                         command.get("set_value")["quest"], 
    240                         command.get("set_value")["variable"], 
    241                         command.get("set_value")["value"]) 
    242  
    243             elif command.get("meet"): 
    244                 self.callbacks["meet"](state, command.get("meet")) 
    245  
    246             elif command.get("get_stuff"): 
    247                 self.callbacks["get_stuff"](state, command.get("get_stuff")) 
    248  
    249             elif command.get("give_stuff"): 
    250                 self.callbacks["give_stuff"](state, command.get("give_stuff")) 
    251  
    252             elif command.get("replace_stuff"): 
    253                 self.callbacks["replace_stuff"](state,  
    254                                                 command.get("replace_stuff")\ 
    255                                                     ["who"], 
    256                                                 command.get("replace_stuff")\ 
    257                                                     ["old_items"], 
    258                                                 command.get("replace_stuff")\ 
    259                                                     ["new_items"]) 
    260             elif command.get("replace_thing"): 
    261                 self.callbacks["replace_thing"](state,  
    262                                                 command.get("replace_thing")\ 
    263                                                     ["who"], 
    264                                                 command.get("replace_thing")\ 
    265                                                     ["old_item"], 
    266                                                 command.get("replace_thing")\ 
    267                                                     ["new_item"]) 
    268  
    269             elif command.get("dialogue"): 
    270                 command = command.get("dialogue") 
    271                 if command == "end": 
    272                     # indicate we"d like to stop talking 
    273                     raise EndException 
    274                 elif command == "back": 
    275                     raise BackException() 
    276                 elif command.startswith("back "): 
    277                     raise BackException(command[5:]) 
    278                 else: 
    279                     raise Exception("Unknown command %s" % (command,)) 
    280  
    281             else: 
    282                 raise Exception("Unknown command %s" % (command,)) 
     63class DialogueResponse(DialogueNode): 
     64    """DialogueNode that represents one possible response to a particular 
     65       DialogueSection.""" 
     66    __slots__ = ['id', 'text', 'actions', 'condition', 'goto_section_id'] 
     67     
     68    def __init__(self, text, next_section_id, actions=None, 
     69                 condition=None): 
     70        DialogueNode.__init__(self, text=text, actions=actions) 
     71        self.condition = condition 
     72        self.next_section_id = next_section_id 
  • trunk/game/scripts/dialogueparsers.py

    r664 r668  
    11#!/usr/bin/env python 
    2  
    32#   This file is part of PARPG. 
    43 
     
    1514#   You should have received a copy of the GNU General Public License 
    1615#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>. 
     16"""Contains classes for parsing and validating @ref Dialogue "Dialogues" and 
     17   other dialogue-related data. 
     18 
     19   @TODO Technomage 2010-11-13: Exception handling + validation needs work. 
     20       Currently YAML files are only crudely validated - the code assumes that 
     21       the file contains valid dialogue data, and if that assumption is 
     22       violated and causes the code to raise any TypeErrors, AttributeErrors or 
     23       ValueErrors the code then raises a DialogueFormatError with the 
     24       original (and mostly unhelpful) error message. 
     25   @TODO Technomage 2010-11-13: Support reading and writing unicode.""" 
     26import logging 
     27try: 
     28    from cStringIO import StringIO 
     29except ImportError: 
     30    from StringIO import StringIO 
     31from collections import Sequence 
     32try: 
     33    from collections import OrderedDict 
     34except ImportError: 
     35    # Python version 2.4-2.6 doesn't have the OrderedDict 
     36    from scripts.common.ordereddict import OrderedDict 
     37import re 
     38import textwrap 
    1739 
    1840import yaml 
    19 import types 
    20 import os 
    21  
    22 class DialogueFormatException(Exception): 
    23     """ Exception thrown when the DialogueValidator has encountered an error""" 
    24     pass 
    25  
    26 class DialogueValidator(object): 
    27     def validateDialogue(self, tree, topdir): 
    28         """Checks whether a given tree is containing a valid dialogue. 
    29            @type tree 
    30            @param tree 
    31            @type topdir: string 
    32            @param topdir: Top directory where to start searching for files. 
    33            @raises: dialogueFormatException on error 
    34            @return: true on success""" 
    35         self.tree = tree  
    36         self.topdir = topdir 
    37  
    38         # Test if the required top nodes are present 
    39         for node in ("NPC", "AVATAR", "START"): 
    40             if not node in self.tree or not isinstance(self.tree[node],\ 
    41                                                        types.StringType): 
    42                 raise DialogueFormatException("Node: " + node + \ 
    43                                               " not found or invalid type") 
    44            
    45         self.__validateAvatar() 
    46  
    47         # Test if the sections node is present  
    48         if not "SECTIONS" in self.tree or not isinstance(self.tree["SECTIONS"],\ 
    49                                                          types.DictionaryType): 
    50             raise DialogueFormatException("Node: SECTIONS not found or invalid") 
    51  
    52         # Start node should be valid  
    53         if not self.tree["START"] in self.tree["SECTIONS"]: 
    54             raise DialogueFormatException("Main section " + self.tree["START"] \ 
    55                                           + " could not be found") 
    56  
    57         # Validate all sections 
    58         for section in self.tree["SECTIONS"]: 
    59             self.__validateSection(self.tree["SECTIONS"][section], section) 
    60              
    61         return True  
    62      
    63     def validateDialogueFromFile(self, file_name, topdir="."): 
    64         """Checks whether a yaml file is containing a valid dialogue  
    65            @type file_name: string  
    66            @param file_name: File name of the yaml file to validate 
    67            @type topdir: string 
    68            @param topdir: Top directory where to start searching for files. 
    69            @raises: DialogueFormatException on error 
    70            @return: True on success""" 
    71         tree = yaml.load(file(file_name)) 
    72         return self.validateDialogue(tree, topdir) 
    73  
    74     def __validateAvatar(self): 
    75         """Check that the avatar is an existing file.  
    76            @raises: DialogueFormatException on error.""" 
    77         fname = os.path.join(self.topdir,self.tree["AVATAR"]) 
    78         if not os.path.isfile(fname): 
    79             raise DialogueFormatException("Avatar file could not " +\ 
    80                                           "be found: " + fname) 
    81  
    82          
    83     def __validateSection(self, section, section_name): 
    84         """Checks whether a section is a valid section. 
    85            @type section: dictionary 
    86            @type section_name: string 
    87            @param section: Section to validate 
    88            @param section_name: Name of the section to validate 
    89            @raises: DialogueFormatException on error """ 
    90         for entry in section: 
    91             # TODO: This section is very repitive, could be coded better 
    92             for action in entry: 
    93                 # Verify if the commands are known and if their parameters have 
    94                 # the correct type. 
    95                 if action == "say": 
    96                     if not isinstance(entry[action], types.StringType): 
    97                         raise DialogueFormatException( 
    98                             "Section: " + section_name + " has an invalid " +\ 
    99                             action + " node") 
    100                 elif action == "responses": 
    101                     if not isinstance(entry[action], types.ListType): 
    102                         raise DialogueFormatException(                         
    103                             "Section: " + section_name + " has an invalid " +\ 
    104                             action + " node") 
    105                     self.__validateResponses(entry["responses"], section_name) 
    106                 elif action == "meet": 
    107                     if not isinstance(entry[action], types.StringType): 
    108                         raise DialogueFormatException(\ 
    109                             "Section: " + section_name + " has an invalid " +\ 
    110                             action + " node") 
    111                 #TODO: verify person  
    112                 elif action in ("complete_quest", "start_quest", \ 
    113                                 "restart_quest", "fail_quest"): 
    114                     if not isinstance(entry[action], types.StringType): 
    115                         raise DialogueFormatException(\ 
    116                             "Section: " + section_name + " has an invalid " +\ 
    117                             action + " node") 
    118                 #TODO: verify quest 
    119                 elif action in ("get_stuff", "give_stuff"): 
    120                     if not isinstance(entry[action], types.StringType): 
    121                         raise DialogueFormatException(\ 
    122                             "Section: " + section_name + " has an invalid " +\ 
    123                             action + " node") 
    124                 #TODO: verify object 
    125                 elif action in ("increase_value", "decrease_value", \ 
    126                                 "set_value"): 
    127                     if not isinstance(entry[action], types.DictionaryType): 
    128                         raise DialogueFormatException(\ 
    129                             "Section: " + section_name + " has an invalid " +\ 
    130                             action + " node")                     
    131                 #TODO: verify value checks  
    132                 elif action == "dialogue": 
    133                     if not isinstance(entry[action], types.StringType) \ 
    134                        or not self.__isValidSectionName(entry[action]): 
    135                         raise DialogueFormatException(\ 
    136                             "Section: " + section_name + " has an invalid " +\ 
    137                             action + " node") 
    138                 else: 
    139                     raise DialogueFormatException("Section: " + section_name + \ 
    140                                                   " has an unknown action: " +\ 
    141                                                   action) 
    142              
    143  
    144     def __validateResponses(self, responses, section_name): 
    145         """Checks if the list of responses is a valid list. 
    146            @type responses: List 
    147            @param respones: A list of response 
    148            @type section_name: String 
    149            @param section_name: The section name these responses belong to 
    150            @raise DialogueFormatException on error  
    151            @return True When the lists is a valid list""" 
    152         for option in responses: 
    153             if not isinstance(option[0], types.StringType): 
    154                 raise DialogueFormatException("Response should be a string") 
    155              
    156             if not self.__isValidSectionName(option[1]): 
    157                 raise DialogueFormatException("Section: " + section_name + \ 
    158                                               " contains an invalid target: " +\ 
    159                                               option[1] + " in response") 
    160             #TODO: option[2] might be a conditional (why?) 
    161          
    162     def __isValidSectionName(self, name): 
    163         """Checks if a given name is valid section 
    164            @type name: string 
    165            @param name: Name of the section to check 
    166            @return True when name is a valid section name, False otherwise""" 
    167         if name=="back" or name=="end": 
    168             return True 
    169  
    170         if name in self.tree["SECTIONS"]: 
    171             return True 
    172  
    173         # Handle 'back name' used to go back 'n' in the stack. 
    174         if name.startswith("back "): 
    175             return self.__isValidSectionName(name.split(" ")[1])         
    176         return False 
     41 
     42from scripts import COPYRIGHT_HEADER 
     43from scripts.dialogue import Dialogue, DialogueSection, DialogueResponse 
     44from scripts.dialogueaction import DialogueAction 
     45 
     46def setup_logging(): 
     47    """Set various logging parameters for this module.""" 
     48    module_logger = logging.getLogger('dialogueparser') 
     49    if (__debug__): 
     50        module_logger.setLevel(logging.DEBUG) 
     51setup_logging() 
     52 
     53class DialogueFormatError(Exception): 
     54    """Exception thrown when the DialogueParser has encountered an error.""" 
     55 
     56 
     57class AbstractDialogueParser(object): 
     58    """Abstract base class defining the interface for parsers responsible for 
     59       constructing a Dialogue from its serialized representation.""" 
     60    def load(self, stream): 
     61        """Parse a stream and attempt to construct a new Dialogue instance 
     62           from its serialized representation. 
     63           @param stream: stream containing the serialized representation of a 
     64               Dialogue 
     65           @type stream: BufferType""" 
     66        raise NotImplementedError('AbstractDialogueParser subclasses must ' 
     67                                  'override the parse method.') 
     68     
     69    def validate(self, stream): 
     70        """Parse a stream and verify that it contains a valid serialization of 
     71           a Dialogue instance. 
     72           @param stream: stream containing the serialized representation of a 
     73               Dialogue 
     74           @type stream: BufferType""" 
     75        raise NotImplementedError('AbstractDialogueParser subclasses must ' 
     76                                  'override the validate method.') 
     77 
     78 
     79class YamlDialogueParser(AbstractDialogueParser): 
     80    logger = logging.getLogger('dialogueparser.OldYamlDialogueParser') 
     81     
     82    def load(self, stream, loader_class=yaml.Loader): 
     83        """Parse a YAML stream and attempt to construct a new Dialogue 
     84           instance. 
     85           @param stream: stream containing the YAML representation of a 
     86               Dialogue 
     87           @type stream: BufferType""" 
     88        loader = loader_class(stream) 
     89        dialogue = self.constructDialogue(loader, loader.get_single_node()) 
     90        return dialogue 
     91     
     92    def dump(self, dialogue, output_stream, dumper_class=yaml.Dumper): 
     93        intermediate_stream = StringIO() 
     94        # KLUDE Technomage 2010-11-16: The "width" argument seems to be broken, 
     95        #     as it doesn't take into about current line indentation and fails 
     96        #     to correctly wrap at word boundaries. 
     97        dumper = dumper_class(intermediate_stream, default_flow_style=False, 
     98                              indent=4, width=99999, line_break='\n', 
     99                              allow_unicode=True, explicit_start=True, 
     100                              explicit_end=True, tags=False) 
     101        dialogue_node = self.representDialogue(dumper, dialogue) 
     102        dumper.open() 
     103        dumper.serialize(dialogue_node) 
     104        dumper.close() 
     105        file_contents = intermediate_stream.getvalue() 
     106         
     107        file_contents = re.sub(r'(\n|\r|\r\n)(\s*)(GOTO: .*)', r'\1\2\3\1\2', 
     108                               file_contents) 
     109        lines = file_contents.splitlines() 
     110        max_line_length = 79 
     111        for i in range(len(lines)): 
     112            line = lines[i] 
     113            match = re.match( 
     114                r'^(\s*(?:-\s+)?)(SAY|REPLY|CONDITION):\s+"(.*)"$', 
     115                line 
     116            ) 
     117            if (match and len(line) > max_line_length): 
     118                # Wrap long lines for readability. 
     119                initial_indent = len(match.group(1)) 
     120                subsequent_indent = initial_indent + 4 
     121                text_wrapper = textwrap.TextWrapper( 
     122                    max_line_length, 
     123                    subsequent_indent=' ' * subsequent_indent, 
     124                    break_long_words=False, 
     125                    break_on_hyphens=False 
     126                ) 
     127                new_lines = text_wrapper.wrap(line) 
     128                new_lines = new_lines[:1] + [re.sub(r'^(\s*) (.*)$', 
     129                                                   r'\1\ \2', l) for l in 
     130                                             new_lines[1:]] 
     131                lines[i] = '\\\n'.join(new_lines) 
     132         
     133        output_stream.write(COPYRIGHT_HEADER) 
     134        output_stream.write('\n'.join(lines)) 
     135         
     136     
     137    def representDialogue(self, dumper, dialogue): 
     138        dialogue_node = dumper.represent_dict({}) 
     139        dialogue_dict = OrderedDict() 
     140        dialogue_dict['NPC_NAME'] = dialogue.npc_name 
     141        dialogue_dict['AVATAR_PATH'] = dialogue.avatar_path 
     142        dialogue_dict['START_SECTION'] = dialogue.start_section_id 
     143        # NOTE Technomage 2010-11-16: Dialogue stores its sections in an 
     144        #     OrderedDict, so a round-trip load, dump, and load will preserve 
     145        #     the order of DialogueSections. 
     146        sections_list_node = dumper.represent_list([]) 
     147        sections_list = sections_list_node.value 
     148        for section in dialogue.sections.values(): 
     149            section_node = self.representDialogueSection(dumper, section) 
     150            sections_list.append(section_node) 
     151        dialogue_dict['SECTIONS'] = sections_list_node 
     152         
     153        for key, value in dialogue_dict.items(): 
     154            if (isinstance(key, yaml.Node)): 
     155                key_node = key 
     156            else: 
     157                key_node = dumper.represent_data(key) 
     158            if (isinstance(value, yaml.Node)): 
     159                value_node = value 
     160            else: 
     161                value_node = dumper.represent_data(value) 
     162            dialogue_node.value.append((key_node, value_node)) 
     163        return dialogue_node 
     164     
     165    def representDialogueSection(self, dumper, dialogue_section): 
     166        section_node = dumper.represent_dict({}) 
     167        section_dict = OrderedDict() # OrderedDict is required to preserve 
     168                                     # the order of attributes. 
     169        section_dict['ID'] = dialogue_section.id 
     170        # KLUDGE Technomage 2010-11-16: Hard-coding the tag like this could be 
     171        #     a problem when writing unicode. 
     172        section_dict['SAY'] = dumper.represent_scalar('tag:yaml.org,2002:str', 
     173                                                      dialogue_section.text, 
     174                                                      style='"') 
     175        actions_list_node = dumper.represent_list([]) 
     176        actions_list = actions_list_node.value 
     177        for action in dialogue_section.actions: 
     178            action_node = self.representDialogueAction(dumper, action) 
     179            actions_list.append(action_node) 
     180        if (actions_list): 
     181            section_dict['ACTIONS'] = actions_list_node 
     182        responses_list_node = dumper.represent_list([]) 
     183        responses_list = responses_list_node.value 
     184        for response in dialogue_section.responses: 
     185            response_node = self.representDialogueResponse(dumper, response) 
     186            responses_list.append(response_node) 
     187        section_dict['RESPONSES'] = responses_list_node 
     188         
     189        for key, value in section_dict.items(): 
     190            if (isinstance(key, yaml.Node)): 
     191                key_node = key 
     192            else: 
     193                key_node = dumper.represent_data(key) 
     194            if (isinstance(value, yaml.Node)): 
     195                value_node = value 
     196            else: 
     197                value_node = dumper.represent_data(value) 
     198            section_node.value.append((key_node, value_node)) 
     199        return section_node 
     200     
     201    def representDialogueResponse(self, dumper, dialogue_response): 
     202        response_node = dumper.represent_dict({}) 
     203        response_dict = OrderedDict() 
     204        # KLUDGE Technomage 2010-11-16: Hard-coding the tag like this could be 
     205        #     a problem when writing unicode. 
     206        response_dict['REPLY'] = dumper.represent_scalar( 
     207            'tag:yaml.org,2002:str', 
     208            dialogue_response.text, 
     209            style='"') 
     210        if (dialogue_response.condition is not None): 
     211            response_dict['CONDITION']  = dumper.represent_scalar( 
     212                'tag:yaml.org,2002:str', 
     213                dialogue_response.condition, 
     214                style='"' 
     215            ) 
     216        actions_list_node = dumper.represent_list([]) 
     217        actions_list = actions_list_node.value 
     218        for action in dialogue_response.actions: 
     219            action_node = self.representDialogueAction(dumper, action) 
     220            actions_list.append(action_node) 
     221        if (actions_list): 
     222            response_dict['ACTIONS'] = actions_list_node 
     223        response_dict['GOTO'] = dialogue_response.next_section_id 
     224         
     225        for key, value in response_dict.items(): 
     226            if (isinstance(key, yaml.Node)): 
     227                key_node = key 
     228            else: 
     229                key_node = dumper.represent_data(key) 
     230            if (isinstance(value, yaml.Node)): 
     231                value_node = value 
     232            else: 
     233                value_node = dumper.represent_data(value) 
     234            response_node.value.append((key_node, value_node)) 
     235        return response_node 
     236     
     237    def representDialogueAction(self, dumper, dialogue_action): 
     238        action_node = dumper.represent_dict({}) 
     239        action_dict = OrderedDict() 
     240        args, kwargs = dialogue_action.arguments 
     241        if (args and not kwargs): 
     242            arguments = list(args) 
     243        elif (kwargs and not args): 
     244            arguments = kwargs 
     245        else: 
     246            arguments = [list(args), kwargs] 
     247        action_dict[dialogue_action.keyword] = arguments 
     248         
     249        for key, value in action_dict.items(): 
     250            if (isinstance(key, yaml.Node)): 
     251                key_node = key 
     252            else: 
     253                key_node = dumper.represent_data(key) 
     254            if (isinstance(value, yaml.Node)): 
     255                value_node = value 
     256            else: 
     257                value_node = dumper.represent_data(value) 
     258            action_node.value.append((key_node, value_node)) 
     259        return action_node 
     260     
     261    def constructDialogue(self, loader, yaml_node): 
     262        npc_name = None 
     263        avatar_path = None 
     264        start_section_id = None 
     265        sections = [] 
     266         
     267        try: 
     268            for key_node, value_node in yaml_node.value: 
     269                key = key_node.value 
     270                if (key == u'NPC_NAME'): 
     271                    npc_name = loader.construct_object(value_node) 
     272                elif (key == u'AVATAR_PATH'): 
     273                    avatar_path = loader.construct_object(value_node) 
     274                elif (key == u'START_SECTION'): 
     275                    start_section_id = loader.construct_object(value_node) 
     276                elif (key == u'SECTIONS'): 
     277                    for section_node in value_node.value: 
     278                        dialogue_section = self.constructDialogueSection( 
     279                            loader, 
     280                            section_node 
     281                        ) 
     282                        sections.append(dialogue_section) 
     283        except (AttributeError, TypeError, ValueError) as e: 
     284            raise DialogueFormatError(e) 
     285         
     286        dialogue = Dialogue(npc_name=npc_name, avatar_path=avatar_path, 
     287                            start_section_id=start_section_id, 
     288                            dialogue_sections=sections) 
     289        return dialogue 
     290     
     291    def constructDialogueSection(self, loader, section_node): 
     292        id = None 
     293        text = None 
     294        responses = [] 
     295        actions = [] 
     296        dialogue_section = None 
     297         
     298        try: 
     299            for key_node, value_node in section_node.value: 
     300                key = key_node.value 
     301                if (key == u'ID'): 
     302                    id = loader.construct_object(value_node) 
     303                elif (key == u'SAY'): 
     304                    text = loader.construct_object(value_node) 
     305                elif (key == u'RESPONSES'): 
     306                    for response_node in value_node.value: 
     307                        dialogue_response = self.constructDialogueResponse( 
     308                            loader, 
     309                            response_node 
     310                        ) 
     311                        responses.append(dialogue_response) 
     312                elif (key == u'ACTIONS'): 
     313                    for action_node in value_node.value: 
     314                        action = self.constructDialogueAction(loader, 
     315                                                             action_node) 
     316                        actions.append(action) 
     317        except (AttributeError, TypeError, ValueError) as e: 
     318            raise DialogueFormatError(e) 
     319        else: 
     320            dialogue_section = DialogueSection(id=id, text=text, 
     321                                               responses=responses, 
     322                                               actions=actions) 
     323         
     324        return dialogue_section 
     325     
     326    def constructDialogueResponse(self, loader, response_node): 
     327        text = None 
     328        next_section_id = None 
     329        actions = [] 
     330        condition = None 
     331         
     332        try: 
     333            for key_node, value_node in response_node.value: 
     334                key = key_node.value 
     335                if (key == u'REPLY'): 
     336                    text = loader.construct_object(value_node) 
     337                elif (key == u'ACTIONS'): 
     338                    for action_node in value_node.value: 
     339                        action = self.constructDialogueAction(loader, 
     340                                                             action_node) 
     341                        actions.append(action) 
     342                elif (key == u'CONDITION'): 
     343                    condition = loader.construct_object(value_node) 
     344                elif (key == u'GOTO'): 
     345                    next_section_id = loader.construct_object(value_node) 
     346        except (AttributeError, TypeError, ValueError) as e: 
     347            raise DialogueFormatError(e) 
     348         
     349        dialogue_response = DialogueResponse(text=text, 
     350                                             next_section_id=next_section_id, 
     351                                             actions=actions, 
     352                                             condition=condition) 
     353        return dialogue_response 
     354     
     355    def constructDialogueAction(self, loader, action_node): 
     356        mapping = loader.construct_mapping(action_node, deep=True) 
     357        keyword, arguments = mapping.items()[0] 
     358        if (isinstance(arguments, dict)): 
     359            # Got a dictionary of keyword arguments. 
     360            args = () 
     361            kwargs = arguments 
     362        elif (not isinstance(arguments, Sequence) or 
     363              isinstance(arguments, basestring)): 
     364            # Got a single positional argument. 
     365            args = (arguments) 
     366            kwargs = {} 
     367        elif (not len(arguments) == 2 or not isinstance(arguments[1], dict)): 
     368            # Got a list of positional arguments. 
     369            args = arguments 
     370            kwargs = {} 
     371        else: 
     372            self.logger.error( 
     373                '{0} is an invalid DialogueAction argument'.format(arguments) 
     374            ) 
     375            return None 
     376         
     377        action_type = DialogueAction.registered_actions.get(keyword) 
     378        if (action_type is None): 
     379            self.logger.error( 
     380                'no DialogueAction with keyword "{0}"'.format(keyword) 
     381            ) 
     382            dialogue_action = None 
     383        else: 
     384            dialogue_action = action_type(*args, **kwargs) 
     385        return dialogue_action 
     386 
     387 
     388class OldYamlDialogueParser(YamlDialogueParser): 
     389    logger = logging.getLogger('dialogueparser.OldYamlDialogueParser') 
     390     
     391    def __init__(self): 
     392        self.response_actions = {} 
     393     
     394    def load(self, stream): 
     395        dialogue = YamlDialogueParser.load(self, stream) 
     396        # Place all DialogueActions that were in DialogueSections into the 
     397        # DialogueResponse that led to the action's original section. 
     398        for section in dialogue.sections.values(): 
     399            for response in section.responses: 
     400                actions = self.response_actions.get(response.next_section_id) 
     401                if (actions is not None): 
     402                    response.actions = actions 
     403        return dialogue 
     404     
     405    def constructDialogue(self, loader, yaml_node): 
     406        npc_name = None 
     407        avatar_path = None 
     408        start_section_id = None 
     409        sections = [] 
     410         
     411        try: 
     412            for key_node, value_node in yaml_node.value: 
     413                key = key_node.value 
     414                if (key == u'NPC'): 
     415                    npc_name = loader.construct_object(value_node) 
     416                elif (key == u'AVATAR'): 
     417                    avatar_path = loader.construct_object(value_node) 
     418                elif (key == u'START'): 
     419                    start_section_id = loader.construct_object(value_node) 
     420                elif (key == u'SECTIONS'): 
     421                    for id_node, section_node in value_node.value: 
     422                        dialogue_section = self.constructDialogueSection( 
     423                            loader, 
     424                            id_node, 
     425                            section_node 
     426                        ) 
     427                        sections.append(dialogue_section) 
     428        except (AttributeError, TypeError, ValueError) as e: 
     429            raise DialogueFormatError(e) 
     430         
     431        dialogue = Dialogue(npc_name=npc_name, avatar_path=avatar_path, 
     432                            start_section_id=start_section_id, 
     433                            dialogue_sections=sections) 
     434        return dialogue 
     435     
     436    def constructDialogueSection(self, loader, id_node, section_node): 
     437        id = loader.construct_object(id_node) 
     438        text = None 
     439        responses = [] 
     440        actions = [] 
     441        dialogue_section = None 
     442         
     443        try: 
     444            for node in section_node.value: 
     445                key_node, value_node = node.value[0] 
     446                key = key_node.value 
     447                if (key == u'say'): 
     448                    text = loader.construct_object(value_node) 
     449                elif (key == u'meet'): 
     450                    action = self.constructDialogueAction(loader, node) 
     451                    actions.append(action) 
     452                elif (key in [u'start_quest', u'complete_quest', u'fail_quest', 
     453                              u'restart_quest', u'set_value', 
     454                              u'decrease_value', u'increase_value', 
     455                              u'give_stuff', u'get_stuff']): 
     456                    action = self.constructDialogueAction(loader, node) 
     457                    if (id not in self.response_actions.keys()): 
     458                        self.response_actions[id] = [] 
     459                    self.response_actions[id].append(action) 
     460                elif (key == u'responses'): 
     461                    for response_node in value_node.value: 
     462                        dialogue_response = self.constructDialogueResponse( 
     463                            loader, 
     464                            response_node 
     465                        ) 
     466                        responses.append(dialogue_response) 
     467        except (AttributeError, TypeError, ValueError) as e: 
     468            raise DialogueFormatError(e) 
     469        else: 
     470            dialogue_section = DialogueSection(id=id, text=text, 
     471                                               responses=responses, 
     472                                               actions=actions) 
     473         
     474        return dialogue_section 
     475     
     476    def constructDialogueResponse(self, loader, response_node): 
     477        text = None 
     478        next_section_id = None 
     479        actions = [] 
     480        condition = None 
     481         
     482        try: 
     483            text = loader.construct_object(response_node.value[0]) 
     484            next_section_id = loader.construct_object(response_node.value[1]) 
     485            if (len(response_node.value) == 3): 
     486                condition = loader.construct_object(response_node.value[2]) 
     487        except (AttributeError, TypeError, ValueError) as e: 
     488            raise DialogueFormatError(e) 
     489         
     490        dialogue_response = DialogueResponse(text=text, 
     491                                             next_section_id=next_section_id, 
     492                                             actions=actions, 
     493                                             condition=condition) 
     494        return dialogue_response 
     495     
     496    def constructDialogueAction(self, loader, action_node): 
     497        mapping = loader.construct_mapping(action_node, deep=True) 
     498        keyword, arguments = mapping.items()[0] 
     499        if (keyword == 'get_stuff'): 
     500            # Renamed keyword in new syntax. 
     501            keyword = 'take_stuff' 
     502        elif (keyword == 'set_value'): 
     503            keyword = 'set_quest_value' 
     504        elif (keyword == 'increase_value'): 
     505            keyword = 'increase_quest_value' 
     506        elif (keyword == 'decrease_value'): 
     507            keyword = 'decrease_quest_value' 
     508        if (isinstance(arguments, dict)): 
     509            # Got a dictionary of keyword arguments. 
     510            args = () 
     511            kwargs = arguments 
     512        elif (not isinstance(arguments, Sequence) or 
     513              isinstance(arguments, basestring)): 
     514            # Got a single positional argument. 
     515            args = (arguments,) 
     516            kwargs = {} 
     517        elif (not len(arguments) == 2 or not isinstance(arguments[1], dict)): 
     518            # Got a list of positional arguments. 
     519            args = arguments 
     520            kwargs = {} 
     521        else: 
     522            self.logger.error( 
     523                '{0} is an invalid DialogueAction argument'.format(arguments) 
     524            ) 
     525            return None 
     526        action_type = DialogueAction.registered_actions.get(keyword) 
     527        if (action_type is None): 
     528            self.logger.error( 
     529                'no DialogueAction with keyword "{0}"'.format(keyword) 
     530            ) 
     531            dialogue_action = None 
     532        else: 
     533            dialogue_action = action_type(*args, **kwargs) 
     534        return dialogue_action 
  • trunk/game/scripts/gamemodel.py

    r667 r668  
    1919import sys 
    2020import os.path 
     21import logging 
    2122from copy import deepcopy 
    2223 
     
    3132from common.utils import parseBool 
    3233from inventory import Inventory 
     34from scripts.dialogueparser import YamlDialogueParser, DialogueFormatError 
    3335 
    3436try: 
     
    732734        """Searches the dialogue directory for dialogues """ 
    733735        files = locateFiles("*.yaml", self.dialogues_directory) 
    734         for dialogue_file in files: 
    735             dialogue_file = os.path.relpath(dialogue_file).replace("\\", "/") 
    736             dialogues = yaml.load_all(file(dialogue_file, "r")) 
    737             for dialogue in dialogues: 
    738                 self.dialogues[dialogue["NPC"]] = dialogue 
     736        dialogue_parser = YamlDialogueParser() 
     737        for dialogue_filepath in files: 
     738            dialogue_filepath = os.path.relpath(dialogue_filepath) \ 
     739                                .replace("\\", "/") 
     740            # Note Technomage 2010-11-13: the new DialogueEngine uses its own 
     741            #     parser now, YamlDialogueParser. 
     742#            dialogues = yaml.load_all(file(dialogue_file, "r")) 
     743            with file(dialogue_filepath, 'r') as dialogue_file: 
     744                try: 
     745                    dialogue = dialogue_parser.load(dialogue_file) 
     746                except DialogueFormatError as error: 
     747                    logging.error('unable to load dialogue file {0}: {1}' 
     748                                  .format(dialogue_filepath, error)) 
     749                else: 
     750                    self.dialogues[dialogue.npc_name] = dialogue 
     751            # Note Technomage 2010-11-13: the below code is used to load 
     752            #     multiple dialogues from a single file. Is this functionality 
     753            #     used/necessary? 
     754#            for dialogue in dialogues: 
     755#                self.dialogues[dialogue["NPC"]] = dialogue 
  • trunk/game/scripts/gui/dialoguegui.py

    r632 r668  
    1515#   You should have received a copy of the GNU General Public License 
    1616#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>. 
     17import logging 
    1718 
    1819from fife import fife 
     
    2021from fife.extensions.pychan import widgets 
    2122 
    22 from scripts.dialogue import DialogueEngine 
     23from scripts.dialogueengine import DialogueEngine 
     24 
     25def setup_logging(): 
     26    """Set various logging parameters for this module.""" 
     27    module_logger = logging.getLogger('dialoguegui') 
     28    if (__debug__): 
     29        module_logger.setLevel(logging.DEBUG) 
     30setup_logging() 
    2331 
    2432class DialogueGUI(object): 
    2533    """Window that handles the dialogues""" 
     34    logger = logging.getLogger('dialoguegui.DialogueGUI') 
     35     
    2636    def __init__(self, controller, npc, quest_engine, player_character): 
    27         self.controller = controller  
     37        self.active = False 
     38        self.controller = controller 
     39        self.dialogue_gui = pychan.loadXML("gui/dialogue.xml") 
     40        self.npc = npc 
     41        # TODO Technomage 2010-11-10: the QuestEngine should probably be 
     42        #     a singleton-like object, which would avoid all of this instance 
     43        #     handling. 
     44        self.quest_engine = quest_engine 
    2845        self.player_character = player_character 
    29         self.active = False 
    30  
    31         # define dialogue engine callbacks 
    32         def startQuest(state, quest_id): 
    33             """Starts a quest 
    34             @param quest_id: Quest to start 
    35             @type quest_id: str  """ 
    36             print "You've picked up the '%s' quest!" % quest_id 
    37             state['quest'].activateQuest(quest_id) 
    38  
    39         def completeQuest(state, quest_id): 
    40             """Marks a quest as finishes 
    41             @param quest_id: Quest to finish 
    42             @type quest_id: str """ 
    43             print "You've finished the quest %s" % quest_id 
    44             state['quest'].finishQuest(quest_id) 
    45  
    46         def failQuest(state, quest_id): 
    47             """Marks a quest as failed 
    48             @param quest_id: Quest to fail 
    49             @type quest_id: str """ 
    50             print "You've failed quest %s" % quest_id 
    51             state['quest'].failQuest(quest_id) 
    52              
    53         def restartQuest(state, quest_id): 
    54             """Restarts a quest 
    55             @param quest_id: Quest to restart 
    56             @type quest_id: str """ 
    57             print "You've restarted quest %s" % quest_id 
    58             state['quest'].restartQuest(quest_id) 
    59  
    60         def increaseQuestValue(state, quest_id, variable, value): 
    61             """Increases a quest variable by the given value 
    62             @param quest_id: Quest of the variable 
    63             @type quest_id: str 
    64             @param variable: Variable to increase 
    65             @type variable: str 
    66             @param value: Value to increase by 
    67             @type value: number """ 
    68             print "Increased %s by %i" % (variable, value) 
    69             state['quest'][quest_id].increaseValue(variable, value) 
    70  
    71         def decreaseQuestValue(state, quest_id, variable, value): 
    72             """Decreases a quest variable by the given value 
    73             @param quest_id: Quest of the variable 
    74             @type quest_id: str 
    75             @param variable: Variable to decrease 
    76             @type variable: str 
    77             @param value: Value to decrease by 
    78             @type value: number """ 
    79             print "Decreased %s by %i" % (variable, value) 
    80             state['quest'][quest_id].decreaseValue(variable, value) 
    81  
    82         def setValue(state, quest_id, variable, value): 
    83             """Sets quest variable to the given value 
    84             @param quest_id: Quest of the variable 
    85             @type quest_id: str 
    86             @param variable: Variable to set 
    87             @type variable: str 
    88             @param value: Value to set to 
    89             @type value: Depends on variable """ 
    90             print "Set %s to %s" % (variable, value) 
    91             state['quest'][quest_id].setValue(variable, value) 
    92  
    93         def meet(state, npc): 
    94             """Marks a non-player-character as met 
    95             @param npc: The non-player-character 
    96             @type npc: str """ 
    97             print "You've met %s!" % npc 
    98             state['pc'].meet(npc) 
    99  
    100         def getStuff(state, thing): 
    101             """Moves an item from the npc inventory to the players 
    102             @param thing: item_type of the item 
    103             @type thing: str """ 
    104             item = state['npc'].inventory.findItem(item_type = thing) 
    105             if item: 
    106                 state['npc'].give(item, state['pc']) 
    107                 print "You've now have the %s" % thing 
    108             else: 
    109                 print "NPC doesn't have the %s" % thing 
    110  
    111         def giveStuff(state, thing): 
    112             """Moves an item from the pc inventory to the npcs 
    113             @param thing: item_type of the item 
    114             @type thing: str """ 
    115             """Moves an item from the npc inventory to the players 
    116             @param thing: item_type of the item 
    117             @type thing: str """ 
    118             item = state['pc'].inventory.findItem(item_type = thing) 
    119             if item: 
    120                 state['pc'].give(item, state['npc']) 
    121                 print "%s now has the %s" % (state['npc'].name, thing) 
    122             else : 
    123                 print "You don't have the %s" % thing 
    124  
    125         def replaceStuff(state, who, old_items, new_items): 
    126             """Replaces an item in the inventory of either the pc or npc 
    127             with another 
    128             @param who: Sets whose item to replace. Should be either pc or npc 
    129             @type who: str 
    130             @param old_items: item_types of the items to replace 
    131             @type old_item: list of str 
    132             @param new_item: Types of the items that replace the old items 
    133             @type new_item: list of str """ 
    134             old_item_instances = [] 
    135             for item_type in old_items: 
    136                 old_item_instances.append(state[who].inventory.\ 
    137                                                 findItem(item_type = item_type)) 
    138             if old_item_instances: 
    139                 for old_item in old_item_instances: 
    140                     state[who].inventory.removeItem(old_item) 
    141                 print "You no longer have " + ", ".join(old_items) 
    142             if new_items: 
    143                 for new_item in new_items: 
    144                     inst_dict = {} 
    145                     inst_dict["ID"] = new_item 
    146                     inst_dict["object_type"] = new_item 
    147                     new_item = self.controller.model.\ 
    148                                 createContainerObject(inst_dict) 
    149                     state[who].inventory.placeItem(new_item) 
    150                 print "You now have " + ", ".join(new_items)         
    151  
    152         def replaceThing(state, who, old_item, new_item): 
    153             """Replaces an item in the inventory of either the pc or npc 
    154             with another 
    155             @param who: Sets whose item to replace. Should be either pc or npc 
    156             @type who: str 
    157             @param old_item: item_type of the item to replace 
    158             @type old_item: str 
    159             @param new_item: Type of the item that replaces the old item 
    160             @type new_item: str """ 
    161             old_item = state[who].inventory.findItem(item_type = old_item) 
    162             if old_item: 
    163                 inst_dict = {} 
    164                 inst_dict["ID"] = new_item 
    165                 inst_dict["object_type"] = new_item 
    166                 new_item = self.controller.model.\ 
    167                             createContainerObject(inst_dict) 
    168                 state[who].inventory.replaceItem(old_item, new_item) 
    169                 print "You no longer have the %s" % old_item.name 
    170                 print "You've now have the %s" % new_item.name         
    171          
    172         dialogue_callbacks = { 
    173             'complete_quest': completeQuest, 
    174             'decrease_quest_value': decreaseQuestValue, 
    175             'fail_quest'  : failQuest, 
    176             'end'           : self.handleEnd, 
    177             'get_stuff'     : getStuff, 
    178             'increase_quest_value': increaseQuestValue, 
    179             'meet'          : meet, 
    180             'npc_name'      : self.handleNpcName, 
    181             'npc_avatar'    : self.handleAvatarImage, 
    182             'responses'     : self.handleResponses, 
    183             'say'           : self.handleSay, 
    184             'set_value'     : setValue, 
    185             'start_quest'   : startQuest, 
    186             "restart_quest" : restartQuest, 
    187             'give_stuff'    : giveStuff, 
    188             'replace_stuff' : replaceStuff, 
    189             'replace_thing' : replaceThing 
    190              
    191         } 
    192  
    193         self.npc = npc 
    194         state = { 
    195             'npc': self.npc, 
    196             'pc': self.player_character, 
    197             'quest': quest_engine 
    198         } 
    199         self.dialogue_engine = DialogueEngine(npc.dialogue, 
    200                                               dialogue_callbacks, state) 
    201         self.dialogue_gui = pychan.loadXML("gui/dialogue.xml") 
    202  
     46     
    20347    def initiateDialogue(self): 
    20448        """Callback for starting a quest""" 
     
    20650        stats_label = self.dialogue_gui.findChild(name='stats_label') 
    20751        stats_label.text = u'Name: John Doe\nAn unnamed one' 
    208  
    20952        events = { 
    21053            'end_button': self.handleEnd 
     
    21255        self.dialogue_gui.mapEvents(events) 
    21356        self.dialogue_gui.show() 
    214         #responses_list = self.dialogue_gui.findChild(name='choices_list') 
    215         responses = self.dialogue_engine.run() 
    216         self.setResponses(responses) 
    217  
    218     def handleSay(self, state, say): 
    219         """Callback for NPC speech""" 
     57        self.setNpcName(self.npc.name) 
     58         
     59        game_state = {'npc': self.npc, 'pc': self.player_character, 
     60                      'quest': self.quest_engine} 
     61        DialogueEngine.initiateDialogue(self.npc.dialogue, game_state) 
     62        self.continueDialogue() 
     63     
     64    def setDialogueText(self, text): 
     65        """Set the displayed dialogue text. 
     66           @param text: text to display.""" 
     67        text = unicode(text) 
    22068        speech = self.dialogue_gui.findChild(name='speech') 
    22169        # to append text to npc speech box, uncomment the following line 
    22270        #speech.text = speech.text + "\n-----\n" + unicode(say) 
    223         speech.text = unicode(say) 
    224  
     71        speech.text = text 
     72        self.logger.debug('set dialogue text to "{0}"'.format(text)) 
     73     
     74    def continueDialogue(self): 
     75        """Display the dialogue text and responses for the current 
     76           L{DialogueSection}.""" 
     77        dialogue_text = DialogueEngine.getCurrentDialogueSection().text 
     78        self.setDialogueText(dialogue_text) 
     79        self.responses = DialogueEngine.continueDialogue() 
     80        self.setResponses(self.responses) 
     81     
    22582    def handleEntered(self, *args): 
    226         """Callback for when user hovers over response label""" 
     83        """Callback for when user hovers over response label.""" 
    22784        pass 
    228  
     85     
    22986    def handleExited(self, *args): 
    230         """Callback for when user hovers out of response label""" 
     87        """Callback for when user hovers out of response label.""" 
    23188        pass 
    232  
     89     
    23390    def handleClicked(self, *args): 
    234         """Handle a response being clicked""" 
    235         response = int(args[0].name.replace('response', '')) 
    236         if not self.dialogue_engine.reply(response): 
     91        """Handle a response being clicked.""" 
     92        response_n = int(args[0].name.replace('response', '')) 
     93        response = self.responses[response_n] 
     94        DialogueEngine.reply(response) 
     95        if (not DialogueEngine.in_dialogue): 
    23796            self.handleEnd() 
    238  
     97        else: 
     98            self.continueDialogue() 
     99     
    239100    def handleEnd(self): 
    240         """Handle the end of the conversation being reached. Either from the 
     101        """Handle the end of the conversation being reached, either from the 
    241102           GUI or from within the conversation itself.""" 
    242         self.dialogue_engine = None 
    243103        self.dialogue_gui.hide() 
     104        self.responses = [] 
    244105        self.npc.behaviour.state = 1 
    245106        self.npc.behaviour.idle() 
    246107        self.active = False 
    247  
    248     def handleNpcName(self, name): 
    249         """Callback to handle setting the NPC name. 
    250            @type name: str 
    251            @param name: The name of the NPC to set. """ 
    252          
     108     
     109    def setNpcName(self, name): 
     110        """Set the NPC name to display on the dialogue GUI. 
     111           @param name: name of the NPC to set 
     112           @type name: basestring""" 
     113        name = unicode(name) 
    253114        stats_label = self.dialogue_gui.findChild(name='stats_label') 
    254115        try: 
    255116            (first_name, desc) = name.split(" ", 1) 
    256117            stats_label.text = u'Name: ' + first_name + "\n" + desc 
    257         except(ValueError): 
    258             stats_label.text = u'Name: ' + name  
    259              
     118        except ValueError: 
     119            stats_label.text = u'Name: ' + name 
     120         
    260121        self.dialogue_gui.title = name 
    261          
    262     def handleAvatarImage(self, image): 
    263         """Callback to handle when the dialogue engine wants to set the NPC  
    264         image 
    265            @type image: str 
    266            @param image: filename of avatar image""" 
     122        self.logger.debug('set NPC name to "{0}"'.format(name)) 
     123     
     124    def setAvatarImage(self, image_path): 
     125        """Set the NPC avatar image to display on the dialogue GUI 
     126           @param image_path: filepath to the avatar image 
     127           @type image_path: basestring""" 
    267128        avatar_image = self.dialogue_gui.findChild(name='npc_avatar') 
    268         avatar_image.image = image 
    269  
    270     def handleResponses(self, *args): 
    271         """Callback to handle when the dialogue engine wants to display a new 
    272            list of options""" 
    273         self.setResponses(args[1]) 
    274  
    275     def setResponses(self, responses): 
     129        avatar_image.image = image_path 
     130     
     131    def setResponses(self, dialogue_responses): 
    276132        """Creates the list of clickable response labels and sets their 
    277            respective on-click callbacks 
    278            @type responses: [ [ "response text", section, condition ], ...] 
    279            @param responses: the list of response objects from the dialogue 
    280                              engine""" 
     133           respective on-click callbacks. 
     134           @param responses: list of L{DialogueResponses} from the 
     135               L{DialogueEngine} 
     136           @type responses: list of L{DialogueResponses}""" 
    281137        choices_list = self.dialogue_gui.findChild(name='choices_list') 
    282138        choices_list.removeAllChildren() 
    283         for index, response in enumerate(responses): 
     139        for index, response in enumerate(dialogue_responses): 
    284140            button = widgets.Label( 
    285                 name="response%s"%(index,), 
    286                 text=unicode(response[0]), 
     141                name="response{0}".format(index), 
     142                text=unicode(response.text), 
    287143                hexpand="1", 
    288144                min_size=(100,16), 
     
    295151            button.border_size = 0 
    296152            button.wrap_text = 1 
    297             button.capture(lambda button=button: self.handleEntered(button), \ 
     153            button.capture(lambda button=button: self.handleEntered(button), 
    298154                           event_name='mouseEntered') 
    299             button.capture(lambda button=button: self.handleExited(button), \ 
     155            button.capture(lambda button=button: self.handleExited(button), 
    300156                           event_name='mouseExited') 
    301             button.capture(lambda button=button: self.handleClicked(button), \ 
     157            button.capture(lambda button=button: self.handleClicked(button), 
    302158                           event_name='mouseClicked') 
    303159            choices_list.addChild(button) 
    304160            self.dialogue_gui.adaptLayout(True) 
    305  
    306  
     161            self.logger.debug( 
     162                'added {0} to response choice list'.format(response) 
     163            ) 
Note: See TracChangeset for help on using the changeset viewer.