Mercurial > skaapsteker
diff skaapsteker/dialogue.py @ 153:704d23022f09
Start of dialogue tree / NPC state machine support.
author | Simon Cross <hodgestar@gmail.com> |
---|---|
date | Tue, 05 Apr 2011 22:18:26 +0200 |
parents | |
children | 13e10b877f6c |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/skaapsteker/dialogue.py Tue Apr 05 22:18:26 2011 +0200 @@ -0,0 +1,126 @@ +import json + +from . import data + + +class DSM(object): + """Dialogue State Machine! + + Parameters + ---------- + json_filename : str + Path to file under data/ that contains JSON description + of state machine. + world : object + Something to allow states to introspect the game state with. + """ + + def __init__(self, json_filename, world): + self.world = world + self.state = "start" + self.states = AttrDict() + src = json.loads(data.load(json_filename).read()) + for state, state_src in src.iteritems(): + pseudo_path = [json_filename, state] + self.states[state] = DsmState(state, state_src, pseudo_path) + assert self.state in self.states, "DSM must have start state" + + def get_state(self): + return self.states[self.state] + + def event(self, ev): + my_locals = { + "state": self.states, + "world" : self.world, + } + my_locals.update(ev.items) + state = self.states[self.state] + next_state = state.event(my_locals) + if next_state.name in self.states: + self.state = next_state.name + + def choice(self, i): + self.event(DsmEvent(choice=i)) + + +class AttrDict(dict): + + def __getattr__(self, name): + if name not in self: + raise AttributeError("No attribute %r" % (name,)) + return self[name] + + +class DsmEvent(object): + + def __init__(self, choice=None): + self.items = { + "choice": choice, + } + + +class DsmState(object): + """State within a DSM. + """ + + def __init__(self, name, state_src, base_path): + self.name = name + self.text = state_src.get("text", None) + self.choices = [] + self.triggers = [] + + choices = state_src.get("choices", []) + for i, choice in enumerate(choices): + pseudo_path = base_path + ["choice-%d" % i] + self.choices.append((i, choice["text"])) + next_state_code = choice.get("next", None) + if next_state_code is not None: + self.triggers.append( + Trigger("choice == %d" % i, next_state_code, pseudo_path)) + + events = state_src.get("events", []) + for i, event in enumerate(events): + pseudo_path = base_path + ["event-%d" % i] + self.triggers.append(Trigger(event["matches"], event["next"], + pseudo_path)) + + auto_next = state_src.get("auto_next", None) + if auto_next is not None: + pseudo_path = base_path + ["auto_next"] + self.triggers.append(Trigger("""True""", auto_next, pseudo_path)) + + def __repr__(self): + return "<%r name=%r>" % (self.__class__.__name__, self.name) + + def event(self, my_locals): + for trigger in self.triggers: + next_state = trigger.fire(my_locals) + if next_state is not None: + return next_state + + +class Trigger(object): + """Matches DSM events and triggers state transitions. + """ + + def __init__(self, matches_code, next_state_code, pseudo_path): + self._matches = compile(matches_code, + "<%s>" % ":".join(pseudo_path + ["match"]), + "eval") + self._next_state = compile(next_state_code, + "<%s>" % ":".join(pseudo_path + ["next"]), + "eval") + + def fire(self, my_locals): + if eval(self._matches, {}, my_locals.copy()): + return eval(self._next_state, {}, my_locals.copy()) + return None + + +class DummyWorld(object): + + def __init__(self): + self._fox_has_tea = False + + def fox_has_tea(self): + return self._fox_has_tea