Changeset 684


Ignore:
Timestamp:
12/10/10 19:11:29 (9 years ago)
Author:
technomage
Message:

Patch by Technomage

  • Refactored the DialogueEngine? singleton into the DialogueProcessor? instance factory, eliminating some of the problems the singleton pattern caused with encapsulation and unit testing.
  • Renamed the dialogueengine.py module to dialogueprocessor.py for clarity.
  • Added type-checking code to the instance variables defined by the DialogueProcessor? and Dialogue classes using assert statements that are removed when the optimization '-O' flag is passed to the Python interpreter.
  • Moved the 'main' logic in dialogue_demo.py script into a 'main' method to avoid cluttering the module namespace with runtime variables.
  • Fixed a few code documentation issues pointed out by aspidites.
Location:
trunk/game
Files:
5 edited
1 moved

Legend:

Unmodified
Added
Removed
  • trunk/game

    • Property svn:ignore
      •  

        old new  
        55settings-old.xml 
        66settings.xml 
         7config.py 
  • trunk/game/dialogue_demo.py

    r682 r684  
    2424from scripts.common.optionparser import OptionParser, OptionError 
    2525from scripts.dialogueparsers import YamlDialogueParser 
    26 from scripts.dialogueengine import DialogueEngine 
     26from scripts.dialogueprocessor import DialogueProcessor 
    2727from scripts.quest_engine import QuestEngine 
     28 
     29def setupLogging(): 
     30    """Set various logging parameters for this module.""" 
     31    logging.basicConfig(filename='dialogue_demo.log') 
     32setupLogging() 
    2833 
    2934PARPG_ROOT_DIR = os.path.dirname(__file__) 
     
    3136DIALOGUE_DIR = os.path.join(PARPG_ROOT_DIR, 'dialogue') 
    3237"""Absolute path to the dialogue directory of the PARPG installation.""" 
     38USAGE_MESSAGE = '''\ 
     39usage: dialogue_demo.py [-h] [dialogue_file] 
     40Script for testing dialogue files. 
    3341 
    34 def setup_logging(): 
    35     """Set various logging parameters for this module.""" 
    36     logging.basicConfig(filename='dialogue_demo.log') 
    37 setup_logging() 
     42-h, --help                  Show this help message. 
     43dialogue_file               YAML file containing a dialogue; if not specified, 
     44                                the user will be prompted to choose a dialogue 
     45                                file from the dialogue directory. 
     46''' 
    3847 
    3948class MockPlayerCharacter(object): 
     
    115124    return selected_file_path 
    116125 
    117 def getReply(dialogue_responses): 
     126def chooseReply(dialogue_responses): 
    118127    """ 
    119128    Prompt the user to choose a L{DialogueResponse} from a list of valid 
     
    148157    @type game_state: dict of objects 
    149158    """ 
    150     DialogueEngine.initiateDialogue(dialogue, game_state) 
    151159    npc_name = dialogue.npc_name 
    152     while DialogueEngine.in_dialogue: 
    153         responses = DialogueEngine.continueDialogue() 
    154         current_dialogue_section = DialogueEngine.getCurrentDialogueSection() 
    155         dialogue_text = current_dialogue_section.text.replace('\n', '\n    ') 
     160    dialogue_processor = DialogueProcessor(dialogue, game_state) 
     161    dialogue_processor.initiateDialogue() 
     162    while dialogue_processor.in_dialogue: 
     163        responses = dialogue_processor.continueDialogue() 
     164        current_dialogue_section = \ 
     165            dialogue_processor.getCurrentDialogueSection() 
     166        dialogue_text = current_dialogue_section.text 
     167        # Indent dialogue text after the first line. 
     168        dialogue_text = dialogue_text.replace('\n', '\n    ') 
    156169        print('\n{0}: {1}'.format(npc_name, dialogue_text)) 
    157         chosen_reply = getReply(responses) 
    158         DialogueEngine.reply(chosen_reply) 
     170        chosen_reply = chooseReply(responses) 
     171        dialogue_processor.reply(chosen_reply) 
    159172 
    160 usage_message='''\ 
    161 usage: dialogue_demo.py [-h] [dialogue_file] 
    162 Script for testing dialogue files. 
    163  
    164 -h, --help                  Show this help message. 
    165 dialogue_file               YAML file containing a dialogue; if not specified, 
    166                                 the user will be prompted to choose a dialogue 
    167                                 file from the dialogue directory. 
    168 ''' 
    169  
    170 if __name__ == "__main__": 
    171     option_parser = OptionParser(usage=usage_message) 
     173def main(argv=sys.argv): 
     174    option_parser = OptionParser(usage=USAGE_MESSAGE) 
    172175    for option in option_parser: 
    173176        if (option in ['-h', '--help']): 
     
    190193        dialogue = dialogue_parser.load(dialogue_file) 
    191194    processDialogue(dialogue, game_state) 
     195 
     196if __name__ == "__main__": 
     197    main() 
  • trunk/game/scripts/dialogue.py

    r680 r684  
    5151        self.npc_name = npc_name 
    5252        self.avatar_path = avatar_path 
    53         self.start_section_id = start_section_id 
    5453        self.sections = OrderedDict() 
    5554        if (dialogue_sections is not None): 
    5655            for section in dialogue_sections: 
    5756                self.sections[section.id] = section 
     57        assert start_section_id in self.sections.keys(), \ 
     58            'start_section_id "{0}" not found in specified sections'\ 
     59            .format(start_section_id) 
     60        self.start_section_id = start_section_id 
    5861     
    5962    def __str__(self): 
     
    128131        @param actions: dialogue actions that should be executed if this 
    129132            response is chosen by the player. 
    130         @type actions: list of L{DialogueActions} 
     133        @type actions: list of L{DialogueActions<DialogueAction>} 
    131134        @param condition: Python expression that when evaluated determines 
    132135            whether the L{DialogueResponse} should be displayed to the player 
  • trunk/game/scripts/dialogueactions.py

    r680 r684  
    2121import logging 
    2222 
    23 def setup_logging(): 
     23def setupLogging(): 
    2424    """Set various logging parameters for this module.""" 
    2525    module_logger = logging.getLogger('dialogueaction') 
    2626    if __debug__: 
    2727        module_logger.setLevel(logging.DEBUG) 
    28 setup_logging() 
     28setupLogging() 
    2929 
    3030class DialogueAction(object): 
  • trunk/game/scripts/dialogueprocessor.py

    r680 r684  
    1818Provides the core interface to the dialogue subsystem used to process player 
    1919L{Dialogues<Dialogue>} with NPCs. 
    20  
    21 @author: or1andov (original design) 
    22 @author: M. George Hansen <technopolitica@gmail.com> (redesign and current 
    23     maintainer) 
    2420""" 
    2521import logging 
    2622 
    27 def setup_logging(): 
     23if (__debug__): 
     24    from scripts.dialogue import Dialogue 
     25 
     26def setupLogging(): 
    2827    """Set various logging parameters for this module.""" 
    2928    module_logger = logging.getLogger('dialogueengine') 
    3029    if (__debug__): 
    3130        module_logger.setLevel(logging.DEBUG) 
    32 setup_logging() 
     31setupLogging() 
    3332 
    34 class DialogueEngine(object): 
     33class DialogueProcessor(object): 
    3534    """ 
    3635    Primary interface to the dialogue subsystem used to initiate and process a 
    3736    L{Dialogue} with an NPC. 
    3837     
    39     The L{DialogueEngine} is a singleton class that exposes the interface to 
    40     the dialogue subsystem via class methods and attributes, and so should not 
    41     be instantiated. 
    42      
    43     To begin a dialogue with an NPC the L{DialogueEngine} must first be 
    44     initialized with a L{Dialogue} defining the dialogue data to process and a 
    45     dictionary of Python objects defining the game state for testing of 
    46     response conditionals. Once the L{DialogueEngine} is initialized processing 
    47     of L{DialogueSections<DialogueSection>} and 
     38    To begin a dialogue with an NPC a L{DialogueProcessor} must first be 
     39    instantiated with the dialogue data to process and a dictionary of Python 
     40    objects defining the game state for testing of response conditionals. The 
     41    L{initiateDialogue} must be called to initialized the L{DialogueProcessor}, 
     42    and once it is initialized processing of 
     43    L{DialogueSections<DialogueSection>} and 
    4844    L{DialogueResponses<DialogueResponse>} can be initiated via the 
    4945    L{continueDialogue} and L{reply} class methods. 
     
    5450    processed. Each time L{reply} is called with a L{DialogueResponse} its 
    5551    next_section_id attribute is used to select a new L{DialogueSection} from 
    56     the L{current_dialogue}. The selected L{DialogueSection} is then pushed 
     52    the L{dialogue}. The selected L{DialogueSection} is then pushed 
    5753    onto the end of the L{dialogue_section_stack}, ready to be processed via 
    5854    L{continueDialogue}. The exception to this rule occurs when L{reply} is 
     
    6258    effectively going back to the previous section of dialogue. 
    6359     
    64     The L{DialogueEngine} terminates dialogue processing once L{reply} is 
     60    The L{DialogueProcessor} terminates dialogue processing once L{reply} is 
    6561    called with a L{DialogueResponse} whose next_section_id == 'end'. 
    6662    Processing can also be manually terminated by calling the L{endDialogue} 
     
    6864     
    6965    @note: See the dialogue_demo.py script for a complete example of how the 
    70         L{DialogueEngine} can be used. 
    71      
    72     @cvar current_dialogue: dialogue data currently being processed. 
    73     @type current_dialogue: L{Dialogue} 
    74     @cvar dialogue_section_stack: sections of dialogue that have been or are 
     66        L{DialogueProcessor} can be used. 
     67     
     68    @ivar dialogue: dialogue data currently being processed. 
     69    @type dialogue: L{Dialogue} 
     70    @ivar dialogue_section_stack: sections of dialogue that have been or are 
    7571        currently being processed. 
    7672    @type dialogue_section_stack: list of L{DialogueSections<DialogueSection>} 
    77     @cvar game_state: objects defining the game state that should be made 
     73    @ivar game_state: objects defining the game state that should be made 
    7874        available for testing L{DialogueResponse} conditionals. 
    7975    @type game_state: dict of Python objects 
    80     @cvar in_dialogue: whether a dialogue has been initiated. 
     76    @ivar in_dialogue: whether a dialogue has been initiated. 
    8177    @type in_dialogue: Bool 
    8278     
    8379    Usage: 
    8480    >>> game_state = {'pc': player_character, 'quest': quest_engine} 
    85     >>> DialogueEngine.initiateDialogue(dialogue, game_state) 
    86     >>> while DialogueEngine.in_dialogue: 
    87     ...     valid_responses = DialogueEngine.continueDialogue() 
     81    >>> dialogue_processor = DialogueProcessor(dialogue, game_state) 
     82    >>> dialogue_processor.initiateDialogue() 
     83    >>> while dialogue_processor.in_dialogue: 
     84    ...     valid_responses = dialogue_processor.continueDialogue() 
    8885    ...     response = choose_response(valid_responses) 
    89     ...     DialogueEngine.reply(response) 
     86    ...     dialogue_processor.reply(response) 
    9087    """ 
    91     current_dialogue = None 
    92     dialogue_section_stack = [] 
    93     game_state = {} 
    94     in_dialogue = False 
    95     _logger = logging.getLogger('dialogueengine.DialogueEngine') 
    96      
    97     def __init__(self): 
    98         raise TypeError('DialogueEngine cannot be instantiated') 
    99      
    100     @classmethod 
    101     def initiateDialogue(cls, dialogue, game_state): 
    102         """Initialize the L{DialogueEngine} with a L{Dialogue} to process. 
    103          
    104         If the DialogueEngine has already been initialized and is currently 
    105         processing a L{Dialogue} then L{endDialogue} will be called to 
    106         terminate processing before re-initializing the L{DialogueEngine} with 
    107         the new L{Dialogue}. 
     88    _logger = logging.getLogger('dialogueengine.DialogueProcessor') 
     89     
     90    def dialogue(): 
     91        def fget(self): 
     92            return self._dialogue 
     93         
     94        def fset(self, dialogue): 
     95            assert isinstance(dialogue, Dialogue), \ 
     96                '{0} does not implement Dialogue interface'.format(dialogue) 
     97            self._dialogue = dialogue 
     98         
     99        def fdel(self): 
     100            self._dialogue = None 
     101         
     102        return locals() 
     103    dialogue = property(**dialogue()) 
     104     
     105    def dialogue_section_stack(): 
     106        def fget(self): 
     107            return self._dialogue_section_stack 
     108         
     109        def fdel(self): 
     110            self._dialogue_section_stack = [] 
     111         
     112        return locals() 
     113    dialogue_section_stack = property(**dialogue_section_stack()) 
     114     
     115    def game_state(): 
     116        def fget(self): 
     117            return self._game_state 
     118         
     119        def fdel(self): 
     120            self._game_state = {} 
     121         
     122        return locals() 
     123    game_state = property(**game_state()) 
     124     
     125    def in_dialogue(): 
     126        def fget(self): 
     127            return self._in_dialogue 
     128         
     129        def fset(self, value): 
     130            assert isinstance(value, bool), '{0} is not a bool'.format(value) 
     131            self._in_dialogue = value 
     132         
     133        return locals() 
     134    in_dialogue = property(**in_dialogue()) 
     135     
     136    def __init__(self, dialogue, game_state): 
     137        """ 
     138        Initialize a new L{DialogueProcessor} instance. 
    108139         
    109140        @param dialogue: dialogue data to process. 
     
    113144        @type game_state: dict of objects 
    114145        """ 
    115         if (cls.in_dialogue): 
    116             # DialogueEngine has already been initialized, so end the current 
    117             # dialogue processing before (re-)initialization. 
    118             cls.endDialogue() 
    119         cls.current_dialogue = dialogue 
    120         cls.game_state = game_state 
    121         cls.in_dialogue = True 
    122         cls._logger.info( 
    123             'initiated dialogue {0}'.format(dialogue) 
    124         ) 
     146        self._dialogue_section_stack = [] 
     147        self._dialogue = dialogue 
     148        self._game_state = game_state 
     149        self._in_dialogue = False 
     150     
     151    def initiateDialogue(self): 
     152        """ 
     153        Prepare the L{DialogueProcessor} to process the L{Dialogue} by pushing the 
     154        starting L{DialogueSection} onto the L{dialogue_section_stack}. 
     155        """ 
     156        dialogue = self.dialogue 
    125157        try: 
    126158            start_section_id = dialogue.start_section_id 
    127         except AttributeError, KeyError: 
    128             cls._logger.error(('unable to determine start DialogueSection for ' 
    129                               '{0}').format(dialogue)) 
    130             cls.endDialogue() 
    131         else: 
    132             cls.dialogue_section_stack.append( 
     159            self.dialogue_section_stack.append( 
    133160                dialogue.sections[start_section_id] 
    134161            ) 
    135             cls.current_dialogue = dialogue 
    136      
    137     @classmethod 
    138     def continueDialogue(cls): 
     162        except (AttributeError, KeyError): 
     163            raise TypeError(('unable to determine start DialogueSection for ' 
     164                             '{0}').format(dialogue)) 
     165        else: 
     166            self.in_dialogue = True 
     167            self._logger.info('initiated dialogue {0}'.format(dialogue)) 
     168     
     169    def continueDialogue(self): 
    139170        """ 
    140171        Process the L{DialogueSection} at the top of the 
     
    147178        @rtype: list of L{DialogueResponses<DialogueResponse>} 
    148179        """ 
    149         current_dialogue_section = cls.getCurrentDialogueSection() 
    150         cls.runDialogueActions(current_dialogue_section) 
    151         valid_responses = cls.getValidResponses(current_dialogue_section) 
     180        current_dialogue_section = self.getCurrentDialogueSection() 
     181        self.runDialogueActions(current_dialogue_section) 
     182        valid_responses = self.getValidResponses(current_dialogue_section) 
    152183         
    153184        return valid_responses 
    154185     
    155     @classmethod 
    156     def getCurrentDialogueSection(cls): 
     186    def getCurrentDialogueSection(self): 
    157187        """ 
    158188        Return the L{DialogueSection} at the top of the 
     
    163193        """ 
    164194        try: 
    165             current_dialogue_section = cls.dialogue_section_stack[-1] 
     195            current_dialogue_section = self.dialogue_section_stack[-1] 
    166196        except IndexError: 
    167             cls._logger.error( 
    168                 'no DialogueSections are in the stack: either an error ' 
    169                 'occurred or DialogueEngine.initiateDialogue was not called ' 
    170                 'first') 
     197            self._logger.error('getCurrentDialogueSection called by no ' 
     198                               'DialogueSections are in the stack') 
    171199            current_dialogue_section = None 
    172200         
    173201        return current_dialogue_section 
    174202     
    175     @classmethod 
    176     def runDialogueActions(cls, dialogue_node): 
     203    def runDialogueActions(self, dialogue_node): 
    177204        """ 
    178205        Execute all L{DialogueActions<DialogueActions>} contained by a 
     
    183210        @type dialogue_node: L{DialogueNode} 
    184211        """ 
    185         cls._logger.info('processing commands for {0}'.format(dialogue_node)) 
     212        self._logger.info('processing commands for {0}'.format(dialogue_node)) 
    186213        for command in dialogue_node.actions: 
    187214            try: 
    188                 command(cls.game_state) 
    189             except Exception as error: 
    190                 cls._logger.error('failed to execute DialogueAction {0}: {1}' 
     215                command(self.game_state) 
     216            except (Exception) as error: 
     217                self._logger.error('failed to execute DialogueAction {0}: {1}' 
    191218                                 .format(command.keyword, error)) 
    192219                # TODO Technomage 2010-11-18: Undo previous actions when an 
     
    194221                return 
    195222            else: 
    196                 cls._logger.debug('ran {0} with arguments {1}' 
     223                self._logger.debug('ran {0} with arguments {1}' 
    197224                                 .format(getattr(type(command), '__name__'), 
    198225                                                 command.arguments)) 
    199226     
    200     @classmethod 
    201     def getValidResponses(cls, dialogue_section): 
     227    def getValidResponses(self, dialogue_section): 
    202228        """ 
    203229        Evaluate all L{DialogueResponse} conditions for a L{DialogueSection} 
     
    219245            try: 
    220246                condition_met = condition is None or \ 
    221                                 eval(condition, cls.game_state) 
    222             except Exception as error: 
    223                 cls._logger.error( 
     247                                eval(condition, self.game_state) 
     248            except (Exception) as error: 
     249                self._logger.error( 
    224250                    ('evaluation of condition "{0}" for {1} failed with ' 
    225251                     'error: {2}').format(dialogue_response.condition, 
     
    227253                ) 
    228254            else: 
    229                 cls._logger.debug( 
     255                self._logger.debug( 
    230256                    'condition "{0}" for {1} evaluated to {2}' 
    231257                    .format(dialogue_response.condition, dialogue_response, 
     
    237263        return valid_responses 
    238264     
    239     @classmethod 
    240     def reply(cls, dialogue_response): 
     265    def reply(self, dialogue_response): 
    241266        """ 
    242267        Reply with a L{DialogueResponse}, execute the 
     
    247272        @type dialogue_response: L{DialogueReponse} 
    248273        """ 
    249         cls._logger.info('replied with {0}'.format(dialogue_response)) 
    250         cls.runDialogueActions(dialogue_response) 
     274        self._logger.info('replied with {0}'.format(dialogue_response)) 
     275        self.runDialogueActions(dialogue_response) 
    251276        next_section_id = dialogue_response.next_section_id 
    252277        if (next_section_id == 'back'): 
    253             if (len(cls.dialogue_section_stack) == 1): 
    254                 cls._logger.error('attempted to run goto: back action but ' 
     278            if (len(self.dialogue_section_stack) == 1): 
     279                self._logger.error('attempted to run goto: back action but ' 
    255280                                 'stack does not contain a previous ' 
    256281                                 'DialogueSection') 
    257282            else: 
    258283                try: 
    259                     cls.dialogue_section_stack.pop() 
     284                    self.dialogue_section_stack.pop() 
    260285                except IndexError: 
    261                     cls._logger.error('attempted to run goto: back action but ' 
    262                                      'the stack was empty: most likely ' 
    263                                      'DialogueEngine.initiateDialogue was not ' 
    264                                      'called first') 
     286                    self._logger.error('attempted to run goto: back action ' 
     287                                       'but the stack was empty') 
    265288                else: 
    266                     cls._logger.debug( 
     289                    self._logger.debug( 
    267290                        'ran goto: back action, restored last DialogueSection' 
    268291                    ) 
    269292        elif (next_section_id == 'end'): 
    270             cls.endDialogue() 
    271             cls._logger.debug('ran goto: end action, ended dialogue') 
     293            self.endDialogue() 
     294            self._logger.debug('ran goto: end action, ended dialogue') 
    272295        else: 
    273296            # get a n 
    274297            try: 
    275298                next_dialogue_section = \ 
    276                     cls.current_dialogue.sections[next_section_id] 
     299                    self.dialogue.sections[next_section_id] 
    277300            except KeyError: 
    278                 cls._logger.error(('"{0}" is not a recognized goto: action or ' 
    279                                   'DialogueSection identifier') 
    280                                  .format(next_section_id)) 
     301                self._logger.error( 
     302                    ('"{0}" is not a recognized goto: action or ' 
     303                     'DialogueSection identifier').format(next_section_id) 
     304                ) 
    281305            else: 
    282                 cls.dialogue_section_stack.append(next_dialogue_section) 
    283      
    284     @classmethod 
    285     def endDialogue(cls): 
     306                self.dialogue_section_stack.append(next_dialogue_section) 
     307     
     308    def endDialogue(self): 
    286309        """ 
    287310        End the current dialogue and clean up any resources in use by the 
    288         L{DialogueEngine}. 
    289         """ 
    290         cls.dialogue_stack = [] 
    291         cls.current_dialogue = None 
    292         cls.in_dialogue = False 
     311        L{DialogueProcessor}. 
     312        """ 
     313        del self.dialogue_section_stack 
     314        self.in_dialogue = False 
  • trunk/game/scripts/gui/dialoguegui.py

    r680 r684  
    2121from fife.extensions.pychan import widgets 
    2222 
    23 from scripts.dialogueengine import DialogueEngine 
     23from scripts.dialogueprocessor import DialogueProcessor 
    2424 
    25 def setup_logging(): 
     25def setupLogging(): 
    2626    """Set various logging parameters for this module.""" 
    2727    module_logger = logging.getLogger('dialoguegui') 
    2828    if (__debug__): 
    2929        module_logger.setLevel(logging.DEBUG) 
    30 setup_logging() 
     30setupLogging() 
    3131 
    3232class DialogueGUI(object): 
     
    6060        game_state = {'npc': self.npc, 'pc': self.player_character, 
    6161                      'quest': self.quest_engine} 
    62         DialogueEngine.initiateDialogue(self.npc.dialogue, game_state) 
    63         self.continueDialogue() 
     62        try: 
     63            self.dialogue_processor = DialogueProcessor(self.npc.dialogue, 
     64                                                        game_state) 
     65            self.dialogue_processor.initiateDialogue() 
     66        except (TypeError) as error: 
     67            self._logger.error(str(error)) 
     68        else: 
     69            self.continueDialogue() 
    6470     
    6571    def setDialogueText(self, text): 
     
    7682        """Display the dialogue text and responses for the current 
    7783           L{DialogueSection}.""" 
    78         dialogue_text = DialogueEngine.getCurrentDialogueSection().text 
     84        dialogue_processor = self.dialogue_processor 
     85        dialogue_text = dialogue_processor.getCurrentDialogueSection().text 
    7986        self.setDialogueText(dialogue_text) 
    80         self.responses = DialogueEngine.continueDialogue() 
     87        self.responses = dialogue_processor.continueDialogue() 
    8188        self.setResponses(self.responses) 
    8289     
     
    93100        response_n = int(args[0].name.replace('response', '')) 
    94101        response = self.responses[response_n] 
    95         DialogueEngine.reply(response) 
    96         if (not DialogueEngine.in_dialogue): 
     102        dialogue_processor = self.dialogue_processor 
     103        dialogue_processor.reply(response) 
     104        if (not dialogue_processor.in_dialogue): 
    97105            self.handleEnd() 
    98106        else: 
     
    134142           respective on-click callbacks. 
    135143           @param responses: list of L{DialogueResponses} from the 
    136                L{DialogueEngine} 
     144               L{DialogueProcessor} 
    137145           @type responses: list of L{DialogueResponses}""" 
    138146        choices_list = self.dialogue_gui.findChild(name='choices_list') 
Note: See TracChangeset for help on using the changeset viewer.