source: skaapsteker/dialogue.py

Last change on this file was 612:a91b2e4400a5, checked in by Simon Cross <hodgestar@…>, 12 years ago

Fallback to using simplejson if json does not exist (this appears to be all that is needed to add Python2.5 compatibility).

File size: 7.0 KB
Line 
1try:
2 import json
3except ImportError:
4 import simplejson as json
5
6from . import data
7from .engine import OpenDialog, AddSpriteEvent, ChangeScene
8
9
10class DSM(object):
11 """Dialogue State Machine!
12
13 Parameters
14 ----------
15 json_filename : str
16 Path to file under data/ that contains JSON description
17 of state machine.
18 world : object
19 Something to allow states to introspect the game state with.
20 """
21
22 def __init__(self, name, world, json_filename, state):
23 self.name = name
24 self.state = state
25 self.world = world
26 self._me = getattr(self.world.npcs, name)
27 self.states = AttrDict()
28 src = json.loads(data.load(json_filename).read(), encoding='utf-8')
29 for state, state_src in src.iteritems():
30 pseudo_path = [json_filename, state]
31 self.states[state] = DsmState(state, state_src, pseudo_path)
32 assert self.state in self.states, "DSM must have start state %r" % (self.state,)
33
34 def get_state(self):
35 return self.states[self.state]
36
37 def has_text(self):
38 self.poke()
39 return bool(self.states[self.state].text)
40
41 def _switch_dialogue_to(self, npc_name):
42 """Switch dialogue to another npc."""
43 OpenDialog.post(npc_name)
44
45 def _drop_item(self, item, shift=(1, 0)):
46 """Create a tail of the given type."""
47 to_level = self._me.level
48 to_pos = self._me.pos
49 to_pos = to_pos[0] + shift[0], to_pos[1] + shift[1]
50 gamestate = self.world.gamestate()
51 sprite = gamestate.create_item_sprite(item, to_level=to_level, to_pos=to_pos)
52 AddSpriteEvent.post(sprite)
53
54 def _end_game(self):
55 """End the game"""
56 from .cutscene import VictoryCutScene
57 ChangeScene.post(VictoryCutScene(None, None))
58
59 def _make_locals(self):
60 my_locals = {
61 "state": self.states,
62 "world": self.world,
63 "npcs": self.world.npcs,
64 "switch_to": self._switch_dialogue_to,
65 "drop_item": self._drop_item,
66 "end_game": self._end_game,
67 }
68 return my_locals
69
70 def choices(self):
71 my_locals = self._make_locals()
72 state = self.states[self.state]
73 return state.choices(my_locals)
74
75 def event(self, ev):
76 my_locals = self._make_locals()
77 my_locals.update(ev.items)
78 state = self.states[self.state]
79 next_state = state.event(my_locals)
80 if next_state is not None and next_state.name in self.states:
81 self.states[self.state].leave(my_locals)
82 self.state = next_state.name
83 self._me.state = self.state
84 self.states[self.state].enter(my_locals)
85
86 def choice(self, i):
87 self.event(DsmEvent(choice=i))
88
89 def auto_next(self):
90 self.event(DsmEvent(auto_next=True))
91
92 def poke(self):
93 # poke the current state to see if it feels like making
94 # a transition.
95 self.event(DsmEvent(poke=True))
96
97
98class AttrDict(dict):
99
100 def __getattr__(self, name):
101 if name not in self:
102 raise AttributeError("No attribute %r" % (name,))
103 return self[name]
104
105
106class DsmEvent(object):
107
108 def __init__(self, choice=None, auto_next=False, poke=False):
109 self.items = {
110 "choice": choice,
111 "auto_next": auto_next,
112 "poke": poke,
113 }
114
115
116class DsmState(object):
117 """State within a DSM.
118 """
119
120 def __init__(self, name, state_src, base_path):
121 self.name = name
122 self.text = state_src.get("text", None)
123 self._choices = []
124 self.triggers = []
125
126 choices = state_src.get("choices", [])
127 for i, choice in enumerate(choices):
128 pseudo_path = base_path + ["choice-%d" % i]
129 if "if" in choice:
130 choice_if = compile(choice["if"],
131 "<%s>" % ":".join(pseudo_path + ["if"]),
132 "eval")
133 else:
134 choice_if = None
135 self._choices.append((i, choice["text"], choice_if))
136 next_state_code = choice.get("next", None)
137 if next_state_code is not None:
138 self.triggers.append(
139 Trigger("choice == %d" % i, next_state_code, pseudo_path))
140
141 events = state_src.get("events", [])
142 for i, event in enumerate(events):
143 pseudo_path = base_path + ["event-%d" % i]
144 self.triggers.append(Trigger(event["matches"], event["next"],
145 pseudo_path))
146
147 auto_next = state_src.get("auto_next", None)
148 self.auto_next_text = state_src.get("auto_next_text", None)
149 self.auto_next = False
150 if auto_next is not None:
151 self.auto_next = True
152 assert not self._choices, "%s: auto_next and choices are not compatible" % ":".join(base_path)
153 pseudo_path = base_path + ["auto_next"]
154 self.triggers.append(Trigger("""auto_next""", auto_next, pseudo_path))
155
156 on_entry = state_src.get("on_entry", None)
157 if on_entry is not None:
158 self.on_entry = compile(on_entry,
159 "<%s>" % ":".join(base_path + ["on_entry"]),
160 "exec")
161 else:
162 self.on_entry = None
163
164 on_exit = state_src.get("on_exit", None)
165 if on_exit is not None:
166 self.on_exit = compile(on_exit,
167 "<%s>" % ":".join(base_path + ["on_exit"]),
168 "exec")
169 else:
170 self.on_exit = None
171
172 def __repr__(self):
173 return "<%r name=%r>" % (self.__class__.__name__, self.name)
174
175 def choices(self, my_locals):
176 for i, text, choice_if in self._choices:
177 if choice_if is None:
178 yield i, text
179 elif eval(choice_if, {}, my_locals.copy()):
180 yield i, text
181
182 def event(self, my_locals):
183 for trigger in self.triggers:
184 next_state = trigger.fire(my_locals)
185 if next_state is not None:
186 return next_state
187
188 def enter(self, my_locals):
189 if self.on_entry is not None:
190 exec(self.on_entry, {}, my_locals.copy())
191
192 def leave(self, my_locals):
193 if self.on_exit is not None:
194 exec(self.on_exit, {}, my_locals.copy())
195
196
197class Trigger(object):
198 """Matches DSM events and triggers state transitions.
199 """
200
201 def __init__(self, matches_code, next_state_code, pseudo_path):
202 self._matches = compile(matches_code,
203 "<%s>" % ":".join(pseudo_path + ["match"]),
204 "eval")
205 self._next_state = compile(next_state_code,
206 "<%s>" % ":".join(pseudo_path + ["next"]),
207 "eval")
208
209 def fire(self, my_locals):
210 if eval(self._matches, {}, my_locals.copy()):
211 return eval(self._next_state, {}, my_locals.copy())
212 return None
Note: See TracBrowser for help on using the repository browser.