Mercurial > parpg-core
view src/parpg/dialogueprocessor.py @ 150:3fc7cfa80771
Modified InventoryGrid to set a name for each slot containing the index.
Added getSlot method to InventoryGrid.
Renamed InventoryGUI class to CharacterGUI.
Added InventoryGUI class which handles the inventory part of the CharacterGUI.
An InventoryGUI instance is now created in CharacterGUI.
author | KarstenBock@gmx.net |
---|---|
date | Wed, 05 Oct 2011 12:59:22 +0200 |
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