Ignore:
Timestamp:
12/04/10 03:47:13 (9 years ago)
Author:
technomage
Message:

Patch by Technomage

  • Updated/wrote code documentation for the core dialogue subsystem modules dialogue.py, dialogueengine.py, dialogueactions.py, and dialogueparsers.py.
  • Updated/created flowcharts and UML diagrams explaining the new DialogueEngine? and how it functions.
  • Made a few minor changes to the dialoguegui.py module to make the class logger less visible.
  • Removed the "this is a sample dialogue file" crud from the old_man.yaml test dialogue file.
File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/game/scripts/dialogueengine.py

    r668 r680  
    11#!/usr/bin/env python 
    2  
     2# 
    33#   This file is part of PARPG. 
    4  
     4# 
    55#   PARPG is free software: you can redistribute it and/or modify 
    66#   it under the terms of the GNU General Public License as published by 
    77#   the Free Software Foundation, either version 3 of the License, or 
    88#   (at your option) any later version. 
    9  
     9# 
    1010#   PARPG is distributed in the hope that it will be useful, 
    1111#   but WITHOUT ANY WARRANTY; without even the implied warranty of 
    1212#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
    1313#   GNU General Public License for more details. 
    14  
     14# 
    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/>. 
     17""" 
     18Provides the core interface to the dialogue subsystem used to process player 
     19L{Dialogues<Dialogue>} with NPCs. 
    1720 
     21@author: or1andov (original design) 
     22@author: M. George Hansen <technopolitica@gmail.com> (redesign and current 
     23    maintainer) 
     24""" 
    1825import logging 
    1926 
     
    2532setup_logging() 
    2633 
    27 class EndException(Exception): 
    28     """EndException is used to bail out from a deeply nested 
    29        runSection/continueWithResponse call stack and end the 
    30        conversation""" 
    31     pass 
    32  
    33 class ResponseException(Exception): 
    34     """ResponseException is used to bail out from a deeply nested 
    35        runSection/continueWithResponse call stack and allow the user to 
    36        specify a response""" 
    37     pass 
    38  
    39 class BackException(Exception): 
    40     """BackException is used to bail out from a deeply nested 
    41        runSection/continueWithResponse call stack and rewind the section 
    42        stack""" 
    43     pass 
    44  
    4534class DialogueEngine(object): 
    46     logger = logging.getLogger('dialogueengine.DialogueEngine') 
    47     game_state = {} 
     35    """ 
     36    Primary interface to the dialogue subsystem used to initiate and process a 
     37    L{Dialogue} with an NPC. 
     38     
     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 
     48    L{DialogueResponses<DialogueResponse>} can be initiated via the 
     49    L{continueDialogue} and L{reply} class methods. 
     50     
     51    The state of dialogue processing is stored via the 
     52    L{dialogue_section_stack} class attribute, which stores a list of 
     53    L{DialogueSections<DialogueSection>} that have been or are currently being 
     54    processed. Each time L{reply} is called with a L{DialogueResponse} its 
     55    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 
     57    onto the end of the L{dialogue_section_stack}, ready to be processed via 
     58    L{continueDialogue}. The exception to this rule occurs when L{reply} is 
     59    called with a L{DialogueResponse} whose next_section_id attribute is "end" 
     60    or "back". "end" terminates the dialogue as described below, while "back" 
     61    removes the last L{DialogueSection} on the L{dialogue_section_stack} 
     62    effectively going back to the previous section of dialogue. 
     63     
     64    The L{DialogueEngine} terminates dialogue processing once L{reply} is 
     65    called with a L{DialogueResponse} whose next_section_id == 'end'. 
     66    Processing can also be manually terminated by calling the L{endDialogue} 
     67    class method. 
     68     
     69    @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 
     75        currently being processed. 
     76    @type dialogue_section_stack: list of L{DialogueSections<DialogueSection>} 
     77    @cvar game_state: objects defining the game state that should be made 
     78        available for testing L{DialogueResponse} conditionals. 
     79    @type game_state: dict of Python objects 
     80    @cvar in_dialogue: whether a dialogue has been initiated. 
     81    @type in_dialogue: Bool 
     82     
     83    Usage: 
     84    >>> game_state = {'pc': player_character, 'quest': quest_engine} 
     85    >>> DialogueEngine.initiateDialogue(dialogue, game_state) 
     86    >>> while DialogueEngine.in_dialogue: 
     87    ...     valid_responses = DialogueEngine.continueDialogue() 
     88    ...     response = choose_response(valid_responses) 
     89    ...     DialogueEngine.reply(response) 
     90    """ 
    4891    current_dialogue = None 
    4992    dialogue_section_stack = [] 
     93    game_state = {} 
    5094    in_dialogue = False 
     95    _logger = logging.getLogger('dialogueengine.DialogueEngine') 
     96     
     97    def __init__(self): 
     98        raise TypeError('DialogueEngine cannot be instantiated') 
    5199     
    52100    @classmethod 
    53101    def initiateDialogue(cls, dialogue, game_state): 
    54         """Walk through a @ref Dialogue "Dialogue's" @ref DialogueSection 
    55            "DialogueSections" and @ref DialogueResponse "DialogueResponses", 
    56            running any @ref DialogueAction "DialogueActions". 
    57            @param dialogue: Dialogue to walk through.""" 
     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}. 
     108         
     109        @param dialogue: dialogue data to process. 
     110        @type dialogue: L{Dialogue} 
     111        @param game_state: objects defining the game state that should be made 
     112            available for testing L{DialogueResponse} conditions. 
     113        @type game_state: dict of objects 
     114        """ 
     115        if (cls.in_dialogue): 
     116            # DialogueEngine has already been initialized, so end the current 
     117            # dialogue processing before (re-)initialization. 
     118            cls.endDialogue() 
    58119        cls.current_dialogue = dialogue 
    59120        cls.game_state = game_state 
    60121        cls.in_dialogue = True 
    61         cls.logger.info( 
     122        cls._logger.info( 
    62123            'initiated dialogue {0}'.format(dialogue) 
    63124        ) 
     
    65126            start_section_id = dialogue.start_section_id 
    66127        except AttributeError, KeyError: 
    67             cls.logger.error(('unable to determine start DialogueSection for ' 
     128            cls._logger.error(('unable to determine start DialogueSection for ' 
    68129                              '{0}').format(dialogue)) 
    69130            cls.endDialogue() 
     
    76137    @classmethod 
    77138    def continueDialogue(cls): 
    78         """Process the DialogueSection at the top of the 
    79            dialogue_section_stack, run any @ref DialogueAction 
    80            "DialogueActions" it contains and return a list of valid 
    81            @ref DialogueResponse "DialogueResponses" after evaluating any 
    82            response conditionals. 
    83             
    84            @returns: list of valid @ref DialogueResponse \"DialogueResponses\" 
    85            """ 
     139        """ 
     140        Process the L{DialogueSection} at the top of the 
     141        L{dialogue_section_stack}, run any L{DialogueActions<DialogueActions>} 
     142        it contains and return a list of valid 
     143        L{DialogueResponses<DialogueResponses> after evaluating any response 
     144        conditionals. 
     145         
     146        @returns: valid responses. 
     147        @rtype: list of L{DialogueResponses<DialogueResponse>} 
     148        """ 
    86149        current_dialogue_section = cls.getCurrentDialogueSection() 
    87150        cls.runDialogueActions(current_dialogue_section) 
     
    92155    @classmethod 
    93156    def getCurrentDialogueSection(cls): 
     157        """ 
     158        Return the L{DialogueSection} at the top of the 
     159        L{dialogue_section_stack}. 
     160         
     161        @returns: section of dialogue currently being processed. 
     162        @rtype: L{DialogueSection} 
     163        """ 
    94164        try: 
    95165            current_dialogue_section = cls.dialogue_section_stack[-1] 
    96166        except IndexError: 
    97             cls.logger.error( 
     167            cls._logger.error( 
    98168                'no DialogueSections are in the stack: either an error ' 
    99169                'occurred or DialogueEngine.initiateDialogue was not called ' 
     
    105175    @classmethod 
    106176    def runDialogueActions(cls, dialogue_node): 
    107         """Execute all @ref DialogueAction "DialogueActions" contained by a 
    108            DialogueNode.""" 
    109         cls.logger.info('processing commands for {0}'.format(dialogue_node)) 
     177        """ 
     178        Execute all L{DialogueActions<DialogueActions>} contained by a 
     179        L{DialogueSection} or L{DialogueResponse}. 
     180         
     181        @param dialogue_node: section of dialogue or response containing the 
     182            L{DialogueActions<DialogueAction>} to execute. 
     183        @type dialogue_node: L{DialogueNode} 
     184        """ 
     185        cls._logger.info('processing commands for {0}'.format(dialogue_node)) 
    110186        for command in dialogue_node.actions: 
    111187            try: 
    112188                command(cls.game_state) 
    113189            except Exception as error: 
    114                 cls.logger.error('failed to execute DialogueAction {0}: {1}' 
     190                cls._logger.error('failed to execute DialogueAction {0}: {1}' 
    115191                                 .format(command.keyword, error)) 
    116             else: 
    117                 cls.logger.debug('ran {0} with arguments {1}' 
     192                # TODO Technomage 2010-11-18: Undo previous actions when an 
     193                #     action fails to execute. 
     194                return 
     195            else: 
     196                cls._logger.debug('ran {0} with arguments {1}' 
    118197                                 .format(getattr(type(command), '__name__'), 
    119198                                                 command.arguments)) 
     
    121200    @classmethod 
    122201    def getValidResponses(cls, dialogue_section): 
    123         """Evaluate all DialogueResponse conditions for a DialogueSection 
    124            and return a list of valid responses. 
    125             
    126            @return: list of @ref DialogueResponse "DialogueResponses" whose 
    127                conditions were met""" 
     202        """ 
     203        Evaluate all L{DialogueResponse} conditions for a L{DialogueSection} 
     204        and return a list of valid responses. 
     205         
     206        @param dialogue_section: section of dialogue containing the 
     207            L{DialogueResponses<DialogueResponse>} to process. 
     208        @type dialogue_section: L{DialogueSection} 
     209         
     210        @return: responses whose conditions were met. 
     211        @rtype: list of L{DialogueResponses<DialogueResponse>} 
     212        """ 
    128213        if (dialogue_section is None): 
    129214            # die nicely when the dialogue_section doesn't exist 
     
    136221                                eval(condition, cls.game_state) 
    137222            except Exception as error: 
    138                 cls.logger.error( 
     223                cls._logger.error( 
    139224                    ('evaluation of condition "{0}" for {1} failed with ' 
    140225                     'error: {2}').format(dialogue_response.condition, 
     
    142227                ) 
    143228            else: 
    144                 cls.logger.debug( 
     229                cls._logger.debug( 
    145230                    'condition "{0}" for {1} evaluated to {2}' 
    146231                    .format(dialogue_response.condition, dialogue_response, 
     
    154239    @classmethod 
    155240    def reply(cls, dialogue_response): 
    156         """""" 
    157         cls.logger.info('replied with {0}'.format(dialogue_response)) 
     241        """ 
     242        Reply with a L{DialogueResponse}, execute the 
     243        L{DialogueActions<DialogueAction>} it contains and push the next 
     244        L{DialogueSection} onto the L{dialogue_section_stack}. 
     245         
     246        @param dialogue_response: response to reply with. 
     247        @type dialogue_response: L{DialogueReponse} 
     248        """ 
     249        cls._logger.info('replied with {0}'.format(dialogue_response)) 
    158250        cls.runDialogueActions(dialogue_response) 
    159251        next_section_id = dialogue_response.next_section_id 
    160252        if (next_section_id == 'back'): 
    161253            if (len(cls.dialogue_section_stack) == 1): 
    162                 cls.logger.error('attempted to run goto: back action but ' 
     254                cls._logger.error('attempted to run goto: back action but ' 
    163255                                 'stack does not contain a previous ' 
    164256                                 'DialogueSection') 
     
    167259                    cls.dialogue_section_stack.pop() 
    168260                except IndexError: 
    169                     cls.logger.error('attempted to run goto: back action but ' 
     261                    cls._logger.error('attempted to run goto: back action but ' 
    170262                                     'the stack was empty: most likely ' 
    171263                                     'DialogueEngine.initiateDialogue was not ' 
    172264                                     'called first') 
    173265                else: 
    174                     cls.logger.debug( 
     266                    cls._logger.debug( 
    175267                        'ran goto: back action, restored last DialogueSection' 
    176268                    ) 
    177269        elif (next_section_id == 'end'): 
    178270            cls.endDialogue() 
    179             cls.logger.debug('ran goto: end action, ended dialogue') 
     271            cls._logger.debug('ran goto: end action, ended dialogue') 
    180272        else: 
    181273            # get a n 
     
    184276                    cls.current_dialogue.sections[next_section_id] 
    185277            except KeyError: 
    186                 cls.logger.error(('"{0}" is not a recognized goto: action or ' 
     278                cls._logger.error(('"{0}" is not a recognized goto: action or ' 
    187279                                  'DialogueSection identifier') 
    188280                                 .format(next_section_id)) 
     
    192284    @classmethod 
    193285    def endDialogue(cls): 
    194         """End an initiated dialogue and clean up any resources in use by 
    195            the DialogueEngine.""" 
     286        """ 
     287        End the current dialogue and clean up any resources in use by the 
     288        L{DialogueEngine}. 
     289        """ 
    196290        cls.dialogue_stack = [] 
    197291        cls.current_dialogue = None 
Note: See TracChangeset for help on using the changeset viewer.