source: branches/active/character_customization/game/parpg/dialogueprocessor.py @ 774

Revision 774, 15.6 KB checked in by aspidites, 9 years ago (diff)

Patch by Aspidites

  • removed shebang from all python files except launcher scripts
  • added shebang to pychan_designer
  • Property svn:executable set to *
  • Property svn:mime-type set to text/plain
Line 
1#   This file is part of PARPG.
2#
3#   PARPG is free software: you can redistribute it and/or modify
4#   it under the terms of the GNU General Public License as published by
5#   the Free Software Foundation, either version 3 of the License, or
6#   (at your option) any later version.
7#
8#   PARPG is distributed in the hope that it will be useful,
9#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#   GNU General Public License for more details.
12#
13#   You should have received a copy of the GNU General Public License
14#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>.
15"""
16Provides the core interface to the dialogue subsystem used to process player
17L{Dialogues<Dialogue>} with NPCs.
18"""
19import logging
20
21from parpg.common.utils import dedent_chomp
22
23if (__debug__):
24    from collections import Sequence, MutableMapping
25    from parpg.dialogue import Dialogue
26
27def setupLogging():
28    """Set various logging parameters for this module."""
29    module_logger = logging.getLogger('dialogueengine')
30    if (__debug__):
31        module_logger.setLevel(logging.DEBUG)
32setupLogging()
33
34class DialogueProcessor(object):
35    """
36    Primary interface to the dialogue subsystem used to initiate and process a
37    L{Dialogue} with an NPC.
38   
39    To begin a dialogue with an NPC a L{DialogueProcessor} must first be
40    instantiated with the dialogue data to process and a dictionary of Python
41    objects defining the game state for testing of response conditionals. The
42    L{initiateDialogue} must be called to initialized the L{DialogueProcessor},
43    and once it is initialized processing of
44    L{DialogueSections<DialogueSection>} and
45    L{DialogueResponses<DialogueResponse>} can be initiated via the
46    L{continueDialogue} and L{reply} class methods.
47   
48    The state of dialogue processing is stored via the
49    L{dialogue_section_stack} class attribute, which stores a list of
50    L{DialogueSections<DialogueSection>} that have been or are currently being
51    processed. Each time L{reply} is called with a L{DialogueResponse} its
52    next_section_id attribute is used to select a new L{DialogueSection} from
53    the L{dialogue}. The selected L{DialogueSection} is then pushed
54    onto the end of the L{dialogue_section_stack}, ready to be processed via
55    L{continueDialogue}. The exception to this rule occurs when L{reply} is
56    called with a L{DialogueResponse} whose next_section_id attribute is "end"
57    or "back". "end" terminates the dialogue as described below, while "back"
58    removes the last L{DialogueSection} on the L{dialogue_section_stack}
59    effectively going back to the previous section of dialogue.
60   
61    The L{DialogueProcessor} terminates dialogue processing once L{reply} is
62    called with a L{DialogueResponse} whose next_section_id == 'end'.
63    Processing can also be manually terminated by calling the L{endDialogue}
64    class method.
65   
66    @note: See the dialogue_demo.py script for a complete example of how the
67        L{DialogueProcessor} can be used.
68   
69    @ivar dialogue: dialogue data currently being processed.
70    @type dialogue: L{Dialogue}
71    @ivar dialogue_section_stack: sections of dialogue that have been or are
72        currently being processed.
73    @type dialogue_section_stack: list of L{DialogueSections<DialogueSection>}
74    @ivar game_state: objects defining the game state that should be made
75        available for testing L{DialogueResponse} conditionals.
76    @type game_state: dict of Python objects
77    @ivar in_dialogue: whether a dialogue has been initiated.
78    @type in_dialogue: Bool
79   
80    Usage:
81    >>> game_state = {'pc': player_character, 'quest': quest_engine}
82    >>> dialogue_processor = DialogueProcessor(dialogue, game_state)
83    >>> dialogue_processor.initiateDialogue()
84    >>> while dialogue_processor.in_dialogue:
85    ...     valid_responses = dialogue_processor.continueDialogue()
86    ...     response = choose_response(valid_responses)
87    ...     dialogue_processor.reply(response)
88    """
89    _logger = logging.getLogger('dialogueengine.DialogueProcessor')
90   
91    def dialogue():
92        def fget(self):
93            return self._dialogue
94       
95        def fset(self, dialogue):
96            assert isinstance(dialogue, Dialogue), \
97                '{0} does not implement Dialogue interface'.format(dialogue)
98            self._dialogue = dialogue
99       
100        return locals()
101    dialogue = property(**dialogue())
102   
103    def dialogue_section_stack():
104        def fget(self):
105            return self._dialogue_section_stack
106       
107        def fset(self, new_value):
108            assert isinstance(new_value, Sequence) and not \
109                   isinstance(new_value, basestring), \
110                   'dialogue_section_stack must be a Sequence, not {0}'\
111                   .format(new_value)
112            self._dialogue_section_stack = new_value
113       
114        return locals()
115    dialogue_section_stack = property(**dialogue_section_stack())
116   
117    def game_state():
118        def fget(self):
119            return self._game_state
120       
121        def fset(self, new_value):
122            assert isinstance(new_value, MutableMapping),\
123                   'game_state must be a MutableMapping, not {0}'\
124                   .format(new_value)
125            self._game_state = new_value
126       
127        return locals()
128    game_state = property(**game_state())
129   
130    def in_dialogue():
131        def fget(self):
132            return self._in_dialogue
133       
134        def fset(self, value):
135            assert isinstance(value, bool), '{0} is not a bool'.format(value)
136            self._in_dialogue = value
137       
138        return locals()
139    in_dialogue = property(**in_dialogue())
140   
141    def __init__(self, dialogue, game_state):
142        """
143        Initialize a new L{DialogueProcessor} instance.
144       
145        @param dialogue: dialogue data to process.
146        @type dialogue: L{Dialogue}
147        @param game_state: objects defining the game state that should be made
148            available for testing L{DialogueResponse} conditions.
149        @type game_state: dict of objects
150        """
151        self._dialogue_section_stack = []
152        self._dialogue = dialogue
153        self._game_state = game_state
154        self._in_dialogue = False
155   
156    def getDialogueGreeting(self):
157        """
158        Evaluate the L{RootDialogueSections<RootDialogueSection>} conditions
159        and return the valid L{DialogueSection} which should be displayed
160        first.
161       
162        @return: Valid root dialogue section.
163        @rtype: L{DialogueSection}
164       
165        @raise: RuntimeError - evaluation of a DialogueGreeting condition fails
166            by raising an exception (e.g. due to a syntax error).
167        """
168        dialogue = self.dialogue
169        dialogue_greeting = None
170        for greeting in dialogue.greetings:
171            try:
172                condition_met = eval(greeting.condition, self.game_state)
173            except Exception as exception:
174                error_message = dedent_chomp('''
175                    exception raised in DialogueGreeting {id} condition:
176                    {exception}
177                ''').format(id=greeting.id, exception=exception)
178                self._logger.error(error_message)
179            if (condition_met):
180                dialogue_greeting = greeting
181        if (dialogue_greeting is None):
182            dialogue_greeting = dialogue.default_greeting
183       
184        return dialogue_greeting
185   
186    def initiateDialogue(self):
187        """
188        Prepare the L{DialogueProcessor} to process the L{Dialogue} by pushing
189        the starting L{DialogueSection} onto the L{dialogue_section_stack}.
190       
191        @raise RuntimeError: Unable to determine the root L{DialogueSection}
192            defined by the L{Dialogue}.
193        """
194        if (self.in_dialogue):
195            self.endDialogue()
196        dialogue_greeting = self.getDialogueGreeting()
197        self.dialogue_section_stack.append(dialogue_greeting)
198        self.in_dialogue = True
199        self._logger.info('initiated dialogue {0}'.format(self.dialogue))
200   
201    def continueDialogue(self):
202        """
203        Process the L{DialogueSection} at the top of the
204        L{dialogue_section_stack}, run any L{DialogueActions<DialogueActions>}
205        it contains and return a list of valid
206        L{DialogueResponses<DialogueResponses> after evaluating any response
207        conditionals.
208       
209        @returns: valid responses.
210        @rtype: list of L{DialogueResponses<DialogueResponse>}
211       
212        @raise RuntimeError: Any preconditions are not met.
213       
214        @precondition: dialogue has been initiated via L{initiateDialogue}.
215        """
216        if (not self.in_dialogue):
217            error_message = dedent_chomp('''
218                dialogue has not be initiated via initiateDialogue yet
219            ''')
220            raise RuntimeError(error_message)
221        current_dialogue_section = self.getCurrentDialogueSection()
222        self.runDialogueActions(current_dialogue_section)
223        valid_responses = self.getValidResponses(current_dialogue_section)
224       
225        return valid_responses
226   
227    def getCurrentDialogueSection(self):
228        """
229        Return the L{DialogueSection} at the top of the
230        L{dialogue_section_stack}.
231       
232        @returns: section of dialogue currently being processed.
233        @rtype: L{DialogueSection}
234       
235        @raise RuntimeError: Any preconditions are not met.
236       
237        @precondition: dialogue has been initiated via L{initiateDialogue} and
238            L{dialogue_section_stack} contains at least one L{DialogueSection}.
239        """
240        if (not self.in_dialogue):
241            error_message = dedent_chomp('''
242                getCurrentDialogueSection called but the dialogue has not been
243                initiated yet
244            ''')
245            raise RuntimeError(error_message)
246        try:
247            current_dialogue_section = self.dialogue_section_stack[-1]
248        except IndexError:
249            error_message = dedent_chomp('''
250                getCurrentDialogueSection called but no DialogueSections are in
251                the stack
252            ''')
253            raise RuntimeError(error_message)
254       
255        return current_dialogue_section
256   
257    def runDialogueActions(self, dialogue_node):
258        """
259        Execute all L{DialogueActions<DialogueActions>} contained by a
260        L{DialogueSection} or L{DialogueResponse}.
261       
262        @param dialogue_node: section of dialogue or response containing the
263            L{DialogueActions<DialogueAction>} to execute.
264        @type dialogue_node: L{DialogueNode}
265        """
266        self._logger.info('processing commands for {0}'.format(dialogue_node))
267        for command in dialogue_node.actions:
268            try:
269                command(self.game_state)
270            except (Exception,) as error:
271                self._logger.error('failed to execute DialogueAction {0}: {1}'
272                                   .format(command.keyword, error))
273                # TODO Technomage 2010-11-18: Undo previous actions when an
274                #     action fails to execute.
275            else:
276                self._logger.debug('ran {0} with arguments {1}'
277                                   .format(getattr(type(command), '__name__'),
278                                           command.arguments))
279   
280    def getValidResponses(self, dialogue_section):
281        """
282        Evaluate all L{DialogueResponse} conditions for a L{DialogueSection}
283        and return a list of valid responses.
284       
285        @param dialogue_section: section of dialogue containing the
286            L{DialogueResponses<DialogueResponse>} to process.
287        @type dialogue_section: L{DialogueSection}
288       
289        @return: responses whose conditions were met.
290        @rtype: list of L{DialogueResponses<DialogueResponse>}
291        """
292        valid_responses = []
293        for dialogue_response in dialogue_section.responses:
294            condition = dialogue_response.condition
295            try:
296                condition_met = condition is None or \
297                                eval(condition, self.game_state)
298            except (Exception,) as exception:
299                error_message = dedent_chomp('''
300                    evaluation of condition {condition} for {response} failed
301                    with error: {exception}
302                ''').format(condition=dialogue_response.condition,
303                            response=dialogue_response, exception=exception)
304                self._logger.error(error_message)
305            else:
306                self._logger.debug(
307                    'condition "{0}" for {1} evaluated to {2}'
308                    .format(dialogue_response.condition, dialogue_response,
309                            condition_met)
310                )
311                if (condition_met):
312                    valid_responses.append(dialogue_response)
313       
314        return valid_responses
315   
316    def reply(self, dialogue_response):
317        """
318        Reply with a L{DialogueResponse}, execute the
319        L{DialogueActions<DialogueAction>} it contains and push the next
320        L{DialogueSection} onto the L{dialogue_section_stack}.
321       
322        @param dialogue_response: response to reply with.
323        @type dialogue_response: L{DialogueReponse}
324       
325        @raise RuntimeError: Any precondition is not met.
326       
327        @precondition: L{initiateDialogue} must be called before this method
328            is used.
329        """
330        if (not self.in_dialogue):
331            error_message = dedent_chomp('''
332                reply cannot be called until the dialogue has been initiated
333                via initiateDialogue
334            ''')
335            raise RuntimeError(error_message)
336        self._logger.info('replied with {0}'.format(dialogue_response))
337        # FIXME: Technomage 2010-12-11: What happens if runDialogueActions
338        #     raises an error?
339        self.runDialogueActions(dialogue_response)
340        next_section_id = dialogue_response.next_section_id
341        if (next_section_id == 'back'):
342            if (len(self.dialogue_section_stack) == 1):
343                error_message = dedent_chomp('''
344                    attempted to run goto: back action but stack does not
345                    contain a previous DialogueSection
346                ''')
347                raise RuntimeError(error_message)
348            else:
349                try:
350                    self.dialogue_section_stack.pop()
351                except (IndexError,):
352                    error_message = dedent_chomp('''
353                        attempted to run goto: back action but the stack was
354                        empty
355                    ''')
356                    raise RuntimeError(error_message)
357                else:
358                    self._logger.debug(
359                        'ran goto: back action, restored last DialogueSection'
360                    )
361        elif (next_section_id == 'end'):
362            self.endDialogue()
363            self._logger.debug('ran goto: end action, ended dialogue')
364        else:
365            try:
366                next_dialogue_section = \
367                    self.dialogue.sections[next_section_id]
368            except KeyError:
369                error_message = dedent_chomp('''
370                    {0} is not a recognized goto: action or DialogueSection
371                    identifier
372                ''').format(next_section_id)
373                raise RuntimeError(error_message)
374            else:
375                self.dialogue_section_stack.append(next_dialogue_section)
376   
377    def endDialogue(self):
378        """
379        End the current dialogue and clean up any resources in use by the
380        L{DialogueProcessor}.
381        """
382        self.dialogue_section_stack = []
383        self.in_dialogue = False
Note: See TracBrowser for help on using the repository browser.