source: trunk/game/scripts/dialogueengine.py @ 668

Revision 668, 7.8 KB checked in by technomage, 9 years ago (diff)

Ticket #269: Patch by Technomage.

  • Working prototype of the redesigned DialogueEngine? for the upcoming Techdemo2 release; major redesign of the existing DialogueEngine? and YAML dialogue file syntax.
  • Moved the redesigned DialogueEngine? class to the more descriptive dialogueengine.py module; the DialogueEngine? is now a singleton object and provides all functionality through class methods and attributes, and thus should not be instantiated
  • Abstracted the data structures used to store dialogue data away from the YAML data structures; the relevant classes are stored in the dialogue.py module
  • Abstracted the dialogue commands/actions from the DialogueEngine? code to make modifications and maintenance of dialogue logic easier; the relevant classes are stored in the dialogueactions.py module
  • The PyYAML loader has been replaced with a more robust YamlDialogueParser? class (see the dialogueparsers.py module) that interfaces with the new dialogue data structure classes and supports the new YAML dialogue file syntax; an OldYamlDialogueParser? class is provided to support reading the old Techdemo1 syntax
  • Removed the existing dialogue validator; runtime dialogue validation is not yet implemented
  • Added the convert_dialogue script, which converts dialogue files in the old Techdemo1 format to the new format; all existing dialogue files have been converted to work with the new parser
  • Added two support modules for the new classes and script: ordereddict.py, which provides a Python 2.7-like OrderedDict? class for Python versions 2.4-2.6; optionparser.py, which is a simplified command-line option parser for writing scripts as an alternative to argparse and optparse
  • Updated the dialogue_demo.py script to work with the new DialogueEngine?
  • fixes[s:trac, t:269]
  • Property svn:mime-type set to text/plain
Line 
1#!/usr/bin/env python
2
3#   This file is part of PARPG.
4
5#   PARPG is free software: you can redistribute it and/or modify
6#   it under the terms of the GNU General Public License as published by
7#   the Free Software Foundation, either version 3 of the License, or
8#   (at your option) any later version.
9
10#   PARPG is distributed in the hope that it will be useful,
11#   but WITHOUT ANY WARRANTY; without even the implied warranty of
12#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#   GNU General Public License for more details.
14
15#   You should have received a copy of the GNU General Public License
16#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>.
17
18import logging
19
20def setup_logging():
21    """Set various logging parameters for this module."""
22    module_logger = logging.getLogger('dialogueengine')
23    if (__debug__):
24        module_logger.setLevel(logging.DEBUG)
25setup_logging()
26
27class 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
33class 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
39class 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
45class DialogueEngine(object):
46    logger = logging.getLogger('dialogueengine.DialogueEngine')
47    game_state = {}
48    current_dialogue = None
49    dialogue_section_stack = []
50    in_dialogue = False
51   
52    @classmethod
53    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."""
58        cls.current_dialogue = dialogue
59        cls.game_state = game_state
60        cls.in_dialogue = True
61        cls.logger.info(
62            'initiated dialogue {0}'.format(dialogue)
63        )
64        try:
65            start_section_id = dialogue.start_section_id
66        except AttributeError, KeyError:
67            cls.logger.error(('unable to determine start DialogueSection for '
68                              '{0}').format(dialogue))
69            cls.endDialogue()
70        else:
71            cls.dialogue_section_stack.append(
72                dialogue.sections[start_section_id]
73            )
74            cls.current_dialogue = dialogue
75   
76    @classmethod
77    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           """
86        current_dialogue_section = cls.getCurrentDialogueSection()
87        cls.runDialogueActions(current_dialogue_section)
88        valid_responses = cls.getValidResponses(current_dialogue_section)
89       
90        return valid_responses
91   
92    @classmethod
93    def getCurrentDialogueSection(cls):
94        try:
95            current_dialogue_section = cls.dialogue_section_stack[-1]
96        except IndexError:
97            cls.logger.error(
98                'no DialogueSections are in the stack: either an error '
99                'occurred or DialogueEngine.initiateDialogue was not called '
100                'first')
101            current_dialogue_section = None
102       
103        return current_dialogue_section
104   
105    @classmethod
106    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))
110        for command in dialogue_node.actions:
111            try:
112                command(cls.game_state)
113            except Exception as error:
114                cls.logger.error('failed to execute DialogueAction {0}: {1}'
115                                 .format(command.keyword, error))
116            else:
117                cls.logger.debug('ran {0} with arguments {1}'
118                                 .format(getattr(type(command), '__name__'),
119                                                 command.arguments))
120   
121    @classmethod
122    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"""
128        if (dialogue_section is None):
129            # die nicely when the dialogue_section doesn't exist
130            return
131        valid_responses = []
132        for dialogue_response in dialogue_section.responses:
133            condition = dialogue_response.condition
134            try:
135                condition_met = condition is None or \
136                                eval(condition, cls.game_state)
137            except Exception as error:
138                cls.logger.error(
139                    ('evaluation of condition "{0}" for {1} failed with '
140                     'error: {2}').format(dialogue_response.condition,
141                                   dialogue_response, error)
142                )
143            else:
144                cls.logger.debug(
145                    'condition "{0}" for {1} evaluated to {2}'
146                    .format(dialogue_response.condition, dialogue_response,
147                            condition_met)
148                )
149                if (condition_met):
150                    valid_responses.append(dialogue_response)
151       
152        return valid_responses
153   
154    @classmethod
155    def reply(cls, dialogue_response):
156        """"""
157        cls.logger.info('replied with {0}'.format(dialogue_response))
158        cls.runDialogueActions(dialogue_response)
159        next_section_id = dialogue_response.next_section_id
160        if (next_section_id == 'back'):
161            if (len(cls.dialogue_section_stack) == 1):
162                cls.logger.error('attempted to run goto: back action but '
163                                 'stack does not contain a previous '
164                                 'DialogueSection')
165            else:
166                try:
167                    cls.dialogue_section_stack.pop()
168                except IndexError:
169                    cls.logger.error('attempted to run goto: back action but '
170                                     'the stack was empty: most likely '
171                                     'DialogueEngine.initiateDialogue was not '
172                                     'called first')
173                else:
174                    cls.logger.debug(
175                        'ran goto: back action, restored last DialogueSection'
176                    )
177        elif (next_section_id == 'end'):
178            cls.endDialogue()
179            cls.logger.debug('ran goto: end action, ended dialogue')
180        else:
181            # get a n
182            try:
183                next_dialogue_section = \
184                    cls.current_dialogue.sections[next_section_id]
185            except KeyError:
186                cls.logger.error(('"{0}" is not a recognized goto: action or '
187                                  'DialogueSection identifier')
188                                 .format(next_section_id))
189            else:
190                cls.dialogue_section_stack.append(next_dialogue_section)
191   
192    @classmethod
193    def endDialogue(cls):
194        """End an initiated dialogue and clean up any resources in use by
195           the DialogueEngine."""
196        cls.dialogue_stack = []
197        cls.current_dialogue = None
198        cls.in_dialogue = False
Note: See TracBrowser for help on using the repository browser.