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

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

Ticket #74: Patch by or1andov, Integrate dialogue GUI and engine into game.

It should now be possible to engage in a conversation with NPC characters by
selecting talk from their context menu. fixes[s:trac, t:74]

Known Issues:


  • the GUI is terrible. instead of a dialog window, we could use a better looking background image and fixed position widgets
  • requires PyYAML to be installed sudo apt-get install python-yaml easy_install PyYAML
  • we have to create a simple state model for the PC can remember tasks
  • need to make NPC's be able to remember things - need a model in NPCs for this
  • no way currently to give or take items from npc's; again, more model work. this could be done by adding another callback similar to start_quest. may be necessary for npc's to have an inventory for this
  • The dialogue engine is not hooked up to specific NPC's... it currently always loads dialogue/sample.yaml - need to have the dialogue looked up from some XML attribute
  • dialogue is not broken off if the PC moves away from the NPC
  • npc image is hardcoded, it should use the image specified in the yaml file
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        try:
65            self.run_section(start_section)
66        except EndException:
67            # we stopped talking to the NPC
68            logging.debug("Reached the end")
69            end_cb = self.callbacks.get('end')
70            if end_cb: end_cb()
71            return
72        except ResponseException, e:
73            return e.args[0]
74        except BackException, e:
75            self.section_stack.pop(-1)
76            try:
77                self.run_section(self.section_stack[-1])
78                return e
79            except ResponseException, e:
80                return e.args[0]
81
82    def get_section(self, section_name):
83        """Return a section object.
84        @type section_name: string
85        @param section_name: The section to get
86        @return: dict"""
87        return self.tree['SECTIONS'][section_name]
88
89    def reply(self, response):
90        """After being prompted to provide a response, reply is called to
91           submit a response.
92           @type choice: int
93           @param choice: the index of the response to submit
94           @return: list of lists (if requesting a response)
95           @return: None (if at the end of the script)"""
96        while True:
97            try:
98                if response is not None:
99                    self.continue_with_response(self.section_stack[-1], response)
100                else:
101                    self.run_section(self.section_stack[-1])
102            except ResponseException, e:
103                logging.debug("Got response exception %s" % (e.args, ))
104                return e.args[0]
105            except BackException, e:
106                # e.args contains the section to jump back to
107                if e.args:
108                    stack = self.section_stack[:]
109                    stack.reverse()
110                    for i,s in enumerate(stack):
111                       if s == e.args[0]:
112                           # remove the end of the section stack up to desired
113                           # section
114                           del self.section_stack[-i:]
115                           break
116                else:
117                    self.section_stack.pop(-1)
118                response = None
119                continue
120            except EndException:
121                end_cb = self.callbacks.get('end')
122                if end_cb: end_cb()
123                logging.debug("Reached the end")
124                return
125
126    def continue_with_response(self, section_name, response):
127        """Reply to a response in a section and continue executing dialogue script
128           @type section_name: str
129           @param section_name: the section to continue
130           @type response: int
131           @param response: the index [0,n-1] of the desired response
132           @raises: EndException on end of script
133           @raises: BackException on "back" reply
134           @return: None"""
135        state = self.state
136        if len(self.section_stack) > 1:
137            if self.section_stack[-1] == self.section_stack[-2]:
138                self.section_stack.pop(-1)
139
140        for command in itertools.cycle(self.get_section(section_name)):
141            if not command.get('responses'): continue
142
143            responses = []
144            for r in command.get('responses'):
145                cond = r[2:]
146                if not cond or eval(cond[0], state, {}):
147                    responses.append(r)
148
149            section = responses[response][1]
150            logging.debug("User chose %s" % (section,))
151
152            if section == "back":
153                raise BackException()
154            elif section.startswith("back "):
155                raise BackException(section[5:])
156            elif section == "end":
157                raise EndException()
158
159            self.run_section(section)
160
161    def run_section(self, section_name):
162        """Run a section
163           @type section_name: string
164           @param section_name: The section to run
165           @return: None
166           @raises: EndException on end of script
167           @raises: BackException on "back" reply"""
168
169        state = self.state
170
171        self.section_stack.append(section_name)
172
173        if len(self.section_stack) > 1:
174            if self.section_stack[-1] == self.section_stack[-2]:
175                self.section_stack.pop(-1)
176
177        logging.debug("In run_section %s %s" % (section_name, self.section_stack,))
178        for command in itertools.cycle(self.get_section(section_name)):
179            if command.get("say"):
180                if self.callbacks.get('say'):
181                    self.callbacks["say"](state, command["say"])
182
183            elif command.get("responses"):
184                responses = []
185                for response in command.get('responses'):
186                    cond = response[2:]
187                    if not cond or eval(cond[0], state, {}):
188                        responses.append(response)
189                if self.callbacks.get("responses"):
190                    self.callbacks["responses"](state, responses)
191
192                raise ResponseException(responses)
193
194            elif command.get("start_quest"):
195                self.callbacks["start_quest"](state, command.get("start_quest"))
196
197            elif command.get("complete_quest"):
198                self.callbacks["complete_quest"](state, command.get("complete_quest"))
199
200            elif command.get("dialogue"):
201                command = command.get("dialogue")
202                if command == "end":
203                    # indicate we"d like to stop talking
204                    raise EndException
205                elif command == "back":
206                    raise BackException()
207                elif command.startswith("back "):
208                    raise BackException(command[5:])
209                else:
210                    raise Exception("Unknown command %s" % (command,))
211
212            else:
213                raise Exception("Unknown command %s %s" % (command,))
Note: See TracBrowser for help on using the repository browser.