source: trunk/game/scripts/dialogue.py @ 290

Revision 290, 7.8 KB checked in by orlandov, 10 years ago (diff)

Add a new test to fix broken unit test introduced by or1andov--

Line 
1#!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
19import sys
20import itertools
21
22class EndException(Exception):
23    """EndException is used to bail out from a deeply nested
24       run_section/continue_with_response call stack and end the
25       conversation"""
26    pass
27
28class ResponseException(Exception):
29    """ResponseException is used to bail out from a deeply nested
30       run_section/continue_with_response call stack and allow the user to
31       specify a response"""
32    pass
33
34class BackException(Exception):
35    """BackException is used to bail out from a deeply nested
36       run_section/continue_with_response call stack and rewind the section
37       stack"""
38    pass
39
40class DialogueEngine(object):
41    def __init__(self, obj, callbacks={}, state={}):
42        """A very simple dialogue engine for a game.
43           d = DialogueEngine(tree, callbacks)
44           d = DialogueEngine('screenplay.yaml', callbacks)"""
45
46        if isinstance(obj, dict):
47            self.tree = obj
48        elif isinstance(obj, str):
49            import yaml
50            self.tree = yaml.load(file(obj))
51
52        logging.basicConfig(level=logging.INFO)
53
54        self.callbacks = callbacks
55        self.state = state
56
57    def run(self):
58        """Start running the dialogue engine.
59        @returns: list of lists (if requesting a response)
60        @returns: None (if at the end of the script)"""
61        start_section = self.tree['START']
62        self.section_stack = []
63
64        self.callbacks['npc_avatar'](self.state, self.tree['AVATAR'])
65
66        try:
67            self.run_section(start_section)
68        except EndException:
69            # we stopped talking to the NPC
70            logging.debug("Reached the end")
71            end_cb = self.callbacks.get('end')
72            if end_cb: end_cb()
73            return
74        except ResponseException, e:
75            return e.args[0]
76        except BackException, e:
77            self.section_stack.pop(-1)
78            try:
79                self.run_section(self.section_stack[-1])
80                return e
81            except ResponseException, e:
82                return e.args[0]
83
84    def get_section(self, section_name):
85        """Return a section object.
86        @type section_name: string
87        @param section_name: The section to get
88        @return: dict"""
89        return self.tree['SECTIONS'][section_name]
90
91    def reply(self, response):
92        """After being prompted to provide a response, reply is called to
93           submit a response.
94           @type choice: int
95           @param choice: the index of the response to submit
96           @return: list of lists (if requesting a response)
97           @return: None (if at the end of the script)"""
98        while True:
99            try:
100                if response is not None:
101                    self.continue_with_response(self.section_stack[-1], response)
102                else:
103                    self.run_section(self.section_stack[-1])
104            except ResponseException, e:
105                logging.debug("Got response exception %s" % (e.args, ))
106                return e.args[0]
107            except BackException, e:
108                # e.args contains the section to jump back to
109                if e.args:
110                    stack = self.section_stack[:]
111                    stack.reverse()
112                    for i,s in enumerate(stack):
113                       if s == e.args[0]:
114                           # remove the end of the section stack up to desired
115                           # section
116                           del self.section_stack[-i:]
117                           break
118                else:
119                    self.section_stack.pop(-1)
120                response = None
121                continue
122            except EndException:
123                end_cb = self.callbacks.get('end')
124                if end_cb: end_cb()
125                logging.debug("Reached the end")
126                return
127
128    def continue_with_response(self, section_name, response):
129        """Reply to a response in a section and continue executing dialogue script
130           @type section_name: str
131           @param section_name: the section to continue
132           @type response: int
133           @param response: the index [0,n-1] of the desired response
134           @raises: EndException on end of script
135           @raises: BackException on "back" reply
136           @return: None"""
137        state = self.state
138        if len(self.section_stack) > 1:
139            if self.section_stack[-1] == self.section_stack[-2]:
140                self.section_stack.pop(-1)
141
142        for command in itertools.cycle(self.get_section(section_name)):
143            if not command.get('responses'): continue
144
145            responses = []
146            for r in command.get('responses'):
147                cond = r[2:]
148                if not cond or eval(cond[0], state, {}):
149                    responses.append(r)
150
151            section = responses[response][1]
152            logging.debug("User chose %s" % (section,))
153
154            if section == "back":
155                raise BackException()
156            elif section.startswith("back "):
157                raise BackException(section[5:])
158            elif section == "end":
159                raise EndException()
160
161            self.run_section(section)
162
163    def run_section(self, section_name):
164        """Run a section
165           @type section_name: string
166           @param section_name: The section to run
167           @return: None
168           @raises: EndException on end of script
169           @raises: BackException on "back" reply"""
170
171        state = self.state
172
173        self.section_stack.append(section_name)
174
175        if len(self.section_stack) > 1:
176            if self.section_stack[-1] == self.section_stack[-2]:
177                self.section_stack.pop(-1)
178
179        logging.debug("In run_section %s %s" % (section_name, self.section_stack,))
180        for command in itertools.cycle(self.get_section(section_name)):
181            if command.get("say"):
182                if self.callbacks.get('say'):
183                    self.callbacks["say"](state, command["say"])
184
185            elif command.get("responses"):
186                responses = []
187                for response in command.get('responses'):
188                    cond = response[2:]
189                    if not cond or eval(cond[0], state, {}):
190                        responses.append(response)
191                if self.callbacks.get("responses"):
192                    self.callbacks["responses"](state, responses)
193
194                raise ResponseException(responses)
195
196            elif command.get("start_quest"):
197                self.callbacks["start_quest"](state, command.get("start_quest"))
198
199            elif command.get("complete_quest"):
200                self.callbacks["complete_quest"](state, command.get("complete_quest"))
201
202            elif command.get("dialogue"):
203                command = command.get("dialogue")
204                if command == "end":
205                    # indicate we"d like to stop talking
206                    raise EndException
207                elif command == "back":
208                    raise BackException()
209                elif command.startswith("back "):
210                    raise BackException(command[5:])
211                else:
212                    raise Exception("Unknown command %s" % (command,))
213
214            else:
215                raise Exception("Unknown command %s %s" % (command,))
Note: See TracBrowser for help on using the repository browser.