Changeset 680 for trunk/game/scripts/dialogueengine.py
- Timestamp:
- 12/04/10 03:47:13 (9 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/game/scripts/dialogueengine.py
r668 r680 1 1 #!/usr/bin/env python 2 2 # 3 3 # This file is part of PARPG. 4 4 # 5 5 # PARPG is free software: you can redistribute it and/or modify 6 6 # it under the terms of the GNU General Public License as published by 7 7 # the Free Software Foundation, either version 3 of the License, or 8 8 # (at your option) any later version. 9 9 # 10 10 # PARPG is distributed in the hope that it will be useful, 11 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 13 # GNU General Public License for more details. 14 14 # 15 15 # You should have received a copy of the GNU General Public License 16 16 # along with PARPG. If not, see <http://www.gnu.org/licenses/>. 17 """ 18 Provides the core interface to the dialogue subsystem used to process player 19 L{Dialogues<Dialogue>} with NPCs. 17 20 21 @author: or1andov (original design) 22 @author: M. George Hansen <technopolitica@gmail.com> (redesign and current 23 maintainer) 24 """ 18 25 import logging 19 26 … … 25 32 setup_logging() 26 33 27 class EndException(Exception):28 """EndException is used to bail out from a deeply nested29 runSection/continueWithResponse call stack and end the30 conversation"""31 pass32 33 class ResponseException(Exception):34 """ResponseException is used to bail out from a deeply nested35 runSection/continueWithResponse call stack and allow the user to36 specify a response"""37 pass38 39 class BackException(Exception):40 """BackException is used to bail out from a deeply nested41 runSection/continueWithResponse call stack and rewind the section42 stack"""43 pass44 45 34 class 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 """ 48 91 current_dialogue = None 49 92 dialogue_section_stack = [] 93 game_state = {} 50 94 in_dialogue = False 95 _logger = logging.getLogger('dialogueengine.DialogueEngine') 96 97 def __init__(self): 98 raise TypeError('DialogueEngine cannot be instantiated') 51 99 52 100 @classmethod 53 101 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() 58 119 cls.current_dialogue = dialogue 59 120 cls.game_state = game_state 60 121 cls.in_dialogue = True 61 cls. logger.info(122 cls._logger.info( 62 123 'initiated dialogue {0}'.format(dialogue) 63 124 ) … … 65 126 start_section_id = dialogue.start_section_id 66 127 except AttributeError, KeyError: 67 cls. logger.error(('unable to determine start DialogueSection for '128 cls._logger.error(('unable to determine start DialogueSection for ' 68 129 '{0}').format(dialogue)) 69 130 cls.endDialogue() … … 76 137 @classmethod 77 138 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 """ 86 149 current_dialogue_section = cls.getCurrentDialogueSection() 87 150 cls.runDialogueActions(current_dialogue_section) … … 92 155 @classmethod 93 156 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 """ 94 164 try: 95 165 current_dialogue_section = cls.dialogue_section_stack[-1] 96 166 except IndexError: 97 cls. logger.error(167 cls._logger.error( 98 168 'no DialogueSections are in the stack: either an error ' 99 169 'occurred or DialogueEngine.initiateDialogue was not called ' … … 105 175 @classmethod 106 176 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)) 110 186 for command in dialogue_node.actions: 111 187 try: 112 188 command(cls.game_state) 113 189 except Exception as error: 114 cls. logger.error('failed to execute DialogueAction {0}: {1}'190 cls._logger.error('failed to execute DialogueAction {0}: {1}' 115 191 .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}' 118 197 .format(getattr(type(command), '__name__'), 119 198 command.arguments)) … … 121 200 @classmethod 122 201 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 """ 128 213 if (dialogue_section is None): 129 214 # die nicely when the dialogue_section doesn't exist … … 136 221 eval(condition, cls.game_state) 137 222 except Exception as error: 138 cls. logger.error(223 cls._logger.error( 139 224 ('evaluation of condition "{0}" for {1} failed with ' 140 225 'error: {2}').format(dialogue_response.condition, … … 142 227 ) 143 228 else: 144 cls. logger.debug(229 cls._logger.debug( 145 230 'condition "{0}" for {1} evaluated to {2}' 146 231 .format(dialogue_response.condition, dialogue_response, … … 154 239 @classmethod 155 240 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)) 158 250 cls.runDialogueActions(dialogue_response) 159 251 next_section_id = dialogue_response.next_section_id 160 252 if (next_section_id == 'back'): 161 253 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 ' 163 255 'stack does not contain a previous ' 164 256 'DialogueSection') … … 167 259 cls.dialogue_section_stack.pop() 168 260 except IndexError: 169 cls. logger.error('attempted to run goto: back action but '261 cls._logger.error('attempted to run goto: back action but ' 170 262 'the stack was empty: most likely ' 171 263 'DialogueEngine.initiateDialogue was not ' 172 264 'called first') 173 265 else: 174 cls. logger.debug(266 cls._logger.debug( 175 267 'ran goto: back action, restored last DialogueSection' 176 268 ) 177 269 elif (next_section_id == 'end'): 178 270 cls.endDialogue() 179 cls. logger.debug('ran goto: end action, ended dialogue')271 cls._logger.debug('ran goto: end action, ended dialogue') 180 272 else: 181 273 # get a n … … 184 276 cls.current_dialogue.sections[next_section_id] 185 277 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 ' 187 279 'DialogueSection identifier') 188 280 .format(next_section_id)) … … 192 284 @classmethod 193 285 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 """ 196 290 cls.dialogue_stack = [] 197 291 cls.current_dialogue = None
Note: See TracChangeset
for help on using the changeset viewer.