Mercurial > parpg-core
view src/parpg/dialogueprocessor.py @ 2:e2a8e3805b04
Made parpg-core the main repository to pull from and build.
* Added parpg-assets as a subrepo under the data folder.
* Moved the SConstruct script into parpg-core and modified it to build and install parpg-assets as well.
* Made a few minor changes to the SConstruct file from the defunct parpg-main repository.
* Modified unix parpg executable script template to be more forgiving of installing the parpg Python package outside of PYTHONPATH.
* Updated the .hgignore file to include temporary SCons files.
author | M. George Hansen <technopolitica@gmail.com> |
---|---|
date | Sun, 15 May 2011 14:51:41 -0700 |
parents | 1fd2201f5c36 |
children |
line wrap: on
line source
# This file is part of PARPG. # # PARPG is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PARPG is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PARPG. If not, see <http://www.gnu.org/licenses/>. """ Provides the core interface to the dialogue subsystem used to process player L{Dialogues<Dialogue>} with NPCs. """ import logging from parpg.common.utils import dedent_chomp if (__debug__): from collections import Sequence, MutableMapping from parpg.dialogue import Dialogue logger = logging.getLogger('dialogueprocessor') class DialogueProcessor(object): """ Primary interface to the dialogue subsystem used to initiate and process a L{Dialogue} with an NPC. To begin a dialogue with an NPC a L{DialogueProcessor} must first be instantiated with the dialogue data to process and a dictionary of Python objects defining the game state for testing of response conditionals. The L{initiateDialogue} must be called to initialized the L{DialogueProcessor}, and once it is initialized processing of L{DialogueSections<DialogueSection>} and L{DialogueResponses<DialogueResponse>} can be initiated via the L{continueDialogue} and L{reply} class methods. The state of dialogue processing is stored via the L{dialogue_section_stack} class attribute, which stores a list of L{DialogueSections<DialogueSection>} that have been or are currently being processed. Each time L{reply} is called with a L{DialogueResponse} its next_section_id attribute is used to select a new L{DialogueSection} from the L{dialogue}. The selected L{DialogueSection} is then pushed onto the end of the L{dialogue_section_stack}, ready to be processed via L{continueDialogue}. The exception to this rule occurs when L{reply} is called with a L{DialogueResponse} whose next_section_id attribute is "end" or "back". "end" terminates the dialogue as described below, while "back" removes the last L{DialogueSection} on the L{dialogue_section_stack} effectively going back to the previous section of dialogue. The L{DialogueProcessor} terminates dialogue processing once L{reply} is called with a L{DialogueResponse} whose next_section_id == 'end'. Processing can also be manually terminated by calling the L{endDialogue} class method. @note: See the dialogue_demo.py script for a complete example of how the L{DialogueProcessor} can be used. @ivar dialogue: dialogue data currently being processed. @type dialogue: L{Dialogue} @ivar dialogue_section_stack: sections of dialogue that have been or are currently being processed. @type dialogue_section_stack: list of L{DialogueSections<DialogueSection>} @ivar game_state: objects defining the game state that should be made available for testing L{DialogueResponse} conditionals. @type game_state: dict of Python objects @ivar in_dialogue: whether a dialogue has been initiated. @type in_dialogue: Bool Usage: >>> game_state = {'pc': player_character, 'quest': quest_engine} >>> dialogue_processor = DialogueProcessor(dialogue, game_state) >>> dialogue_processor.initiateDialogue() >>> while dialogue_processor.in_dialogue: ... valid_responses = dialogue_processor.continueDialogue() ... response = choose_response(valid_responses) ... dialogue_processor.reply(response) """ _logger = logging.getLogger('dialogueengine.DialogueProcessor') def dialogue(): def fget(self): return self._dialogue def fset(self, dialogue): assert isinstance(dialogue, Dialogue), \ '{0} does not implement Dialogue interface'.format(dialogue) self._dialogue = dialogue return locals() dialogue = property(**dialogue()) def dialogue_section_stack(): def fget(self): return self._dialogue_section_stack def fset(self, new_value): assert isinstance(new_value, Sequence) and not \ isinstance(new_value, basestring), \ 'dialogue_section_stack must be a Sequence, not {0}'\ .format(new_value) self._dialogue_section_stack = new_value return locals() dialogue_section_stack = property(**dialogue_section_stack()) def game_state(): def fget(self): return self._game_state def fset(self, new_value): assert isinstance(new_value, MutableMapping),\ 'game_state must be a MutableMapping, not {0}'\ .format(new_value) self._game_state = new_value return locals() game_state = property(**game_state()) def in_dialogue(): def fget(self): return self._in_dialogue def fset(self, value): assert isinstance(value, bool), '{0} is not a bool'.format(value) self._in_dialogue = value return locals() in_dialogue = property(**in_dialogue()) def __init__(self, dialogue, game_state): """ Initialize a new L{DialogueProcessor} instance. @param dialogue: dialogue data to process. @type dialogue: L{Dialogue} @param game_state: objects defining the game state that should be made available for testing L{DialogueResponse} conditions. @type game_state: dict of objects """ self._dialogue_section_stack = [] self._dialogue = dialogue self._game_state = game_state self._in_dialogue = False def getDialogueGreeting(self): """ Evaluate the L{RootDialogueSections<RootDialogueSection>} conditions and return the valid L{DialogueSection} which should be displayed first. @return: Valid root dialogue section. @rtype: L{DialogueSection} @raise: RuntimeError - evaluation of a DialogueGreeting condition fails by raising an exception (e.g. due to a syntax error). """ dialogue = self.dialogue dialogue_greeting = None for greeting in dialogue.greetings: try: condition_met = eval(greeting.condition, self.game_state) except Exception as exception: error_message = dedent_chomp(''' exception raised in DialogueGreeting {id} condition: {exception} ''').format(id=greeting.id, exception=exception) self._logger.error(error_message) if (condition_met): dialogue_greeting = greeting if (dialogue_greeting is None): dialogue_greeting = dialogue.default_greeting return dialogue_greeting def initiateDialogue(self): """ Prepare the L{DialogueProcessor} to process the L{Dialogue} by pushing the starting L{DialogueSection} onto the L{dialogue_section_stack}. @raise RuntimeError: Unable to determine the root L{DialogueSection} defined by the L{Dialogue}. """ if (self.in_dialogue): self.endDialogue() dialogue_greeting = self.getDialogueGreeting() self.dialogue_section_stack.append(dialogue_greeting) self.in_dialogue = True self._logger.info('initiated dialogue {0}'.format(self.dialogue)) def continueDialogue(self): """ Process the L{DialogueSection} at the top of the L{dialogue_section_stack}, run any L{DialogueActions<DialogueActions>} it contains and return a list of valid L{DialogueResponses<DialogueResponses> after evaluating any response conditionals. @returns: valid responses. @rtype: list of L{DialogueResponses<DialogueResponse>} @raise RuntimeError: Any preconditions are not met. @precondition: dialogue has been initiated via L{initiateDialogue}. """ if (not self.in_dialogue): error_message = dedent_chomp(''' dialogue has not be initiated via initiateDialogue yet ''') raise RuntimeError(error_message) current_dialogue_section = self.getCurrentDialogueSection() self.runDialogueActions(current_dialogue_section) valid_responses = self.getValidResponses(current_dialogue_section) return valid_responses def getCurrentDialogueSection(self): """ Return the L{DialogueSection} at the top of the L{dialogue_section_stack}. @returns: section of dialogue currently being processed. @rtype: L{DialogueSection} @raise RuntimeError: Any preconditions are not met. @precondition: dialogue has been initiated via L{initiateDialogue} and L{dialogue_section_stack} contains at least one L{DialogueSection}. """ if (not self.in_dialogue): error_message = dedent_chomp(''' getCurrentDialogueSection called but the dialogue has not been initiated yet ''') raise RuntimeError(error_message) try: current_dialogue_section = self.dialogue_section_stack[-1] except IndexError: error_message = dedent_chomp(''' getCurrentDialogueSection called but no DialogueSections are in the stack ''') raise RuntimeError(error_message) return current_dialogue_section def runDialogueActions(self, dialogue_node): """ Execute all L{DialogueActions<DialogueActions>} contained by a L{DialogueSection} or L{DialogueResponse}. @param dialogue_node: section of dialogue or response containing the L{DialogueActions<DialogueAction>} to execute. @type dialogue_node: L{DialogueNode} """ self._logger.info('processing commands for {0}'.format(dialogue_node)) for command in dialogue_node.actions: try: command(self.game_state) except (Exception,) as error: self._logger.error('failed to execute DialogueAction {0}: {1}' .format(command.keyword, error)) # TODO Technomage 2010-11-18: Undo previous actions when an # action fails to execute. else: self._logger.debug('ran {0} with arguments {1}' .format(getattr(type(command), '__name__'), command.arguments)) def getValidResponses(self, dialogue_section): """ Evaluate all L{DialogueResponse} conditions for a L{DialogueSection} and return a list of valid responses. @param dialogue_section: section of dialogue containing the L{DialogueResponses<DialogueResponse>} to process. @type dialogue_section: L{DialogueSection} @return: responses whose conditions were met. @rtype: list of L{DialogueResponses<DialogueResponse>} """ valid_responses = [] for dialogue_response in dialogue_section.responses: condition = dialogue_response.condition try: condition_met = condition is None or \ eval(condition, self.game_state) except (Exception,) as exception: error_message = dedent_chomp(''' evaluation of condition {condition} for {response} failed with error: {exception} ''').format(condition=dialogue_response.condition, response=dialogue_response, exception=exception) self._logger.error(error_message) else: self._logger.debug( 'condition "{0}" for {1} evaluated to {2}' .format(dialogue_response.condition, dialogue_response, condition_met) ) if (condition_met): valid_responses.append(dialogue_response) return valid_responses def reply(self, dialogue_response): """ Reply with a L{DialogueResponse}, execute the L{DialogueActions<DialogueAction>} it contains and push the next L{DialogueSection} onto the L{dialogue_section_stack}. @param dialogue_response: response to reply with. @type dialogue_response: L{DialogueReponse} @raise RuntimeError: Any precondition is not met. @precondition: L{initiateDialogue} must be called before this method is used. """ if (not self.in_dialogue): error_message = dedent_chomp(''' reply cannot be called until the dialogue has been initiated via initiateDialogue ''') raise RuntimeError(error_message) self._logger.info('replied with {0}'.format(dialogue_response)) # FIXME: Technomage 2010-12-11: What happens if runDialogueActions # raises an error? self.runDialogueActions(dialogue_response) next_section_id = dialogue_response.next_section_id if (next_section_id == 'back'): if (len(self.dialogue_section_stack) == 1): error_message = dedent_chomp(''' attempted to run goto: back action but stack does not contain a previous DialogueSection ''') raise RuntimeError(error_message) else: try: self.dialogue_section_stack.pop() except (IndexError,): error_message = dedent_chomp(''' attempted to run goto: back action but the stack was empty ''') raise RuntimeError(error_message) else: self._logger.debug( 'ran goto: back action, restored last DialogueSection' ) elif (next_section_id == 'end'): self.endDialogue() self._logger.debug('ran goto: end action, ended dialogue') else: try: next_dialogue_section = \ self.dialogue.sections[next_section_id] except KeyError: error_message = dedent_chomp(''' {0} is not a recognized goto: action or DialogueSection identifier ''').format(next_section_id) raise RuntimeError(error_message) else: self.dialogue_section_stack.append(next_dialogue_section) def endDialogue(self): """ End the current dialogue and clean up any resources in use by the L{DialogueProcessor}. """ self.dialogue_section_stack = [] self.in_dialogue = False