diff --git a/npc/pirate/actions.py b/npc/pirate/actions.py index 0abecc3..9b3493a 100644 --- a/npc/pirate/actions.py +++ b/npc/pirate/actions.py @@ -21,16 +21,15 @@ def get_position(entity, memory): return pct.position +class LookAction(CalledOnceContext): + def enter(self): + print "LOOKING" + self.parent.environment.look(self.parent) + + class MoveAction(ActionContext): def update(self, time): - super(MoveAction, self).update(time) - if self.caller.position[1] == self.endpoint: - self.finish() - else: - env = self.caller.environment - path = env.pathfind(self.caller.position[1], self.endpoint) - path.pop() # this will always the the starting position - env.move(self.caller, (env, path.pop())) + print self, "moving?" def setStartpoint(self, pos): self.startpoint = pos @@ -123,3 +122,13 @@ def get_actions(self, caller, memory): exported_actions.append(drink_rum) + + +class look(ActionBuilder): + def get_actions(self, caller, memory): + action = LookAction(caller) + action.effects.append(SimpleGoal(aware=True)) + yield action + + +exported_actions.append(look) diff --git a/pygoap/__init__.py b/pygoap/__init__.py index 8b13789..4fe03b0 100644 --- a/pygoap/__init__.py +++ b/pygoap/__init__.py @@ -1 +1 @@ - +from actions import ActionContext diff --git a/pygoap/actions.py b/pygoap/actions.py index b721142..0dace11 100644 --- a/pygoap/actions.py +++ b/pygoap/actions.py @@ -13,7 +13,7 @@ Actions need to be split into ActionInstances and ActionBuilders. An ActionInstance's job is to work in a planner and to carry out actions. -A ActionBuilder's job is to query the caller and return a list of suitable +A ActionBuilder's job is to query the parent and return a list of suitable actions for the memory. """ @@ -33,10 +33,10 @@ class ActionBuilder(object): tested. Please make sure that the actions are valid. """ - def __call__(self, caller, memory): - return self.get_actions(caller, memory) + def __call__(self, parent, memory): + return self.get_actions(parent, memory) - def get_actions(self, caller, memory): + def get_actions(self, parent, memory): """ Return a list of actions """ @@ -51,8 +51,8 @@ class ActionContext(object): Context where actions take place. """ - def __init__(self, caller, **kwargs): - self.caller = caller + def __init__(self, parent, **kwargs): + self.parent = parent self.state = ACTIONSTATE_NOT_STARTED self.prereqs = [] self.effects = [] @@ -71,9 +71,9 @@ def __exit__(self, *exc): """ Please do not override this method. Use exit instead. """ - if self.state is ACTIONSTATE_RUNNING: + if self.state == ACTIONSTATE_RUNNING: self.state = ACTIONSTATE_FINISHED - if self.state is not ACTIONSTATE_ABORTED: + if not self.state == ACTIONSTATE_ABORTED: self.exit() return False @@ -95,15 +95,24 @@ def update(self, time): """ pass + def finish(self): + """ + Call this method when context is no longer needed or is finished. + Do not override. Handle cleanup in exit instead. + """ + self.state = ACTIONSTATE_FINISHED + def fail(self): """ Call this method if the context is not able to complete + Do not override. Handle cleanup in exit instead. """ self.state = ACTIONSTATE_FAILED def abort(self): """ Call this method to stop this context without cleaning it up + Do not override. Handle cleanup in exit instead. """ self.state = ACTIONSTATE_ABORTED @@ -122,9 +131,9 @@ def test(self, memory=None): modify numerical values, it may be useful to return a fractional value. """ - if memory is None: raise Exception - if not self.prereqs: return 1.0 + + if memory is None: raise Exception values = ( i.test(memory) for i in self.prereqs ) try: @@ -139,7 +148,7 @@ def touch(self, memory=None): Call after the planning phase is complete. """ if memory is None: - memory = self.caller.memory + memory = self.parent.memory [ i.touch(memory) for i in self.effects ] def __repr__(self): @@ -153,8 +162,8 @@ class CalledOnceContext(ActionContext): def __enter__(self): if self.test() == 1.0: - super(CalledOnceContext, self).enter() - super(CalledOnceContext, self).exit() + super(CalledOnceContext, self).__enter__() + super(CalledOnceContext, self).__exit__() else: self.fail() diff --git a/pygoap/agent.py b/pygoap/agent.py index ae393b8..33f8c3c 100644 --- a/pygoap/agent.py +++ b/pygoap/agent.py @@ -1,7 +1,7 @@ from environment import ObjectBase from planning import plan from actions import ActionContext -from blackboard import MemoryManager +from memory import MemoryManager from actionstates import * from precepts import * import logging @@ -11,6 +11,8 @@ NullAction = ActionContext(None) +NullAction.__enter__() +NullAction.__exit__() # required to reduce memory usage def time_filter(precept): @@ -29,11 +31,12 @@ class GoapAgent(ObjectBase): interested = [] idle_timeout = 30 - def __init__(self): + def __init__(self, name=None): + super(GoapAgent, self).__init__(name) self.memory = MemoryManager() self.planner = plan - self.current_goal = None + self.current_goal = None self.goals = [] # all goals this instance can use self.filters = [] # list of methods to use as a filter @@ -61,7 +64,6 @@ def filter_precept(self, precept): precepts can be put through filters to change them. this can be used to simulate errors in judgement by the agent. """ - for f in self.filters: precept = f(precept) if precept is None: @@ -80,7 +82,10 @@ def process(self, precept): debug("[agent] %s recv'd precept %s", self, precept) self.memory.add(precept) - return self.next_action() + if self.next_action is NullAction: + self.replan() + return self.next_action + def replan(self): """ @@ -94,42 +99,62 @@ def replan(self): debug("[agent] goals %s", s) + start_action = NullAction + # starting for the most relevant goal, attempt to make a plan - plan = [] + self.plan = [] for score, goal in s: tentative_plan = self.planner(self, self.actions, - self.current_action, self.memory, goal) + start_action, self.memory, goal) if tentative_plan: + tentative_plan.pop() pretty = list(reversed(tentative_plan[:])) debug("[agent] %s has planned to %s", self, goal) debug("[agent] %s has plan %s", self, pretty) - plan = tentative_plan + self.plan = tentative_plan + self.current_goal = goal break - return plan + + # we only support one concurrent action (i'm lazy) + def running_actions(self): + return self.current_action @property def current_action(self): + """ + get the current action of the current plan + """ + try: return self.plan[-1] except IndexError: return NullAction - def running_actions(self): - return self.current_action - + @property def next_action(self): """ - get the next action of the current plan + if the current action is finished, return the next + otherwise, return the current action """ - if self.plan == []: - self.plan = self.replan() - - # this action is done, so return the next one + # this action is done if self.current_action.state == ACTIONSTATE_FINISHED: - return self.plan.pop() if self.plan else None + + # there are more actions in the queue, so just return the next one + if self.plan: + return self.plan.pop() + + # no more actions, so the plan worked! + else: + + # let the goal do its magic to the memory manager + if self.current_goal: + self.current_goal.touch(self.memory) + self.current_goal = None + + return NullAction # this action failed somehow elif self.current_action.state == ACTIONSTATE_FAILED: @@ -137,6 +162,4 @@ def next_action(self): # our action is still running, just run that elif self.current_action.state == ACTIONSTATE_RUNNING: - return current_action - - + return self.current_action diff --git a/pygoap/environment.py b/pygoap/environment.py index 8989d76..f987271 100644 --- a/pygoap/environment.py +++ b/pygoap/environment.py @@ -49,7 +49,7 @@ def __init__(self, entities=[], agents=[], time=0): self.time = time self._agents = [] self._entities = [] - self._positions = [] + self._positions = {} [ self.add(i) for i in entities ] [ self.add(i) for i in agents ] @@ -67,6 +67,14 @@ def entities(self): def get_position(self, entity): raise NotImplementedError + + # this is a placeholder hack. proper handling will go through + # model_precept() + def look(self, caller): + for i in chain(self._entities, self._agents): + caller.process(LocationPrecept(i, self._positions[i])) + + def run(self, steps=1000): """ Run the Environment for given number of time steps. @@ -81,9 +89,19 @@ def add(self, entity, position=None): from agent import GoapAgent - debug("[env] adding %s", entity) + + # hackish way to force agents to re-evaulate their environment + for a in self._agents: + to_remove = [] + + for p in a.memory.of_class(DatumPrecept): + if p.name == 'aware': + to_remove.append(p) + + [ a.memory.remove(p) for p in to_remove] + # add the agent if isinstance(entity, GoapAgent): self._agents.append(entity) @@ -112,18 +130,17 @@ def update(self, time_passed): p = TimePrecept(self.time) [ a.process(p) for a in self.agents ] - [ self.look(a) for a in self.agents ] - # get all the running actions for the agents self.action_que = [ a.running_actions() for a in self.agents ] + # start any actions that are not started + [ action.__enter__() for action in self.action_que + if action.state == ACTIONSTATE_NOT_STARTED ] + # update all the actions that may be running precepts = [ a.update(time_passed) for a in self.action_que ] precepts = [ p for p in precepts if not p == None ] - - # start any actions that are not started - [ action.enter() for action in self.action_que - if action.state == ACTIONSTATE_NOT_STARTED ] + def broadcast_precepts(self, precepts, agents=None): """ diff --git a/pygoap/environment2d.py b/pygoap/environment2d.py index 4e7c276..ed0c79e 100644 --- a/pygoap/environment2d.py +++ b/pygoap/environment2d.py @@ -81,19 +81,19 @@ def model_vision(self, precept, origin, terminus): def model_sound(self, precept, origin, terminus): return precept - def look(self, caller, direction=None, distance=None): + def look(self, parent, direction=None, distance=None): """ - Simulate vision by sending precepts to the caller. + Simulate vision by sending precepts to the parent. """ model = self.model_precept for entity in self.entities: - caller.process( + parent.process( model( PositionPrecept( entity, self.get_position(entity)), - caller + parent ) ) diff --git a/pygoap/goals.py b/pygoap/goals.py index 83b7790..8592339 100644 --- a/pygoap/goals.py +++ b/pygoap/goals.py @@ -14,7 +14,7 @@ finished successfully. """ -from blackboard import MemoryManager +from memory import MemoryManager from precepts import * import sys, logging diff --git a/pygoap/memory.py b/pygoap/memory.py new file mode 100644 index 0000000..192bd77 --- /dev/null +++ b/pygoap/memory.py @@ -0,0 +1,31 @@ +""" +Memories are stored precepts. +""" + + + +class MemoryManager(set): + """ + Store and manage precepts. + + shared blackboards violate reality in that multiple agents share the same + thoughts, to extend the metaphore. but, the advantage of this is that in + a real-time simulation, it gives the player the impression that the agents + are able to collobroate in some meaningful way, without a significant + impact in performace. + + that being said, i have chosen to restrict blackboards to a per-agent + basis. this library is meant for rpgs, where the action isn't real-time + and would require a more realistic simulation of intelligence. + """ + + def add(self, other): + if len(self) > 20: + self.pop() + super(MemoryManager, self).add(other) + + def of_class(self, klass): + for i in self: + if isinstance(i, klass): + yield i + diff --git a/pygoap/planning.py b/pygoap/planning.py index 3dfcf3c..b8dd3b6 100644 --- a/pygoap/planning.py +++ b/pygoap/planning.py @@ -1,9 +1,9 @@ -from blackboard import MemoryManager +from memory import MemoryManager from actionstates import * from actions import * from heapq import heappop, heappush, heappushpop -from itertools import permutations +from itertools import permutations, chain import logging import sys @@ -11,36 +11,21 @@ -def get_children(caller, parent, builders, dupe_parent=False): - """ - return every other action on this branch that has not already been used - """ - def keep_node(node): - keep = True - node0 = node.parent - while not node0.parent == None: - if node0.parent == node: - keep = False - break - node0 = node0.parent - - return keep - - def get_actions2(builders, caller, parent): - for builder in builders: - for action in builder(caller, parent.memory): - yield PlanningNode(parent, action) +def get_children(parent0, parent, builders): + def get_used_class(node): + while node.parent is not None: + yield node.builder + node = node.parent - def get_actions(builders, caller, parent): - for builder in builders: - for action in builder(caller, parent.memory): - node = PlanningNode(parent, action) - if keep_node(node): - yield node + used_class = set(get_used_class(parent)) - #print list(permutations(get_actions2(builders, caller, parent))) + for builder in builders: + if builder in used_class: + continue - return get_actions(builders, caller, parent) + for action in builder(parent0, parent.memory): + node = PlanningNode(parent, builder, action) + yield node def calcG(node): @@ -55,8 +40,9 @@ class PlanningNode(object): """ """ - def __init__(self, parent, action, memory=None): + def __init__(self, parent, builder, action, memory=None): self.parent = parent + self.builder = builder self.action = action self.memory = MemoryManager() self.delta = MemoryManager() @@ -93,20 +79,17 @@ def __repr__(self): self.cost) -def plan(caller, actions, start_action, start_memory, goal): +def plan(parent, builders, start_action, start_memory, goal): """ - Return a list of actions that could be called to satisfy the goal. + Return a list of builders that could be called to satisfy the goal. + Cannot duplicate builders in the plan """ # the pushback is used to limit node access in the heap pushback = None - keyNode = PlanningNode(None, start_action, start_memory) + keyNode = PlanningNode(None, None, start_action, start_memory) openlist = [(0, keyNode)] - - # the root can return a copy of itself, the others cannot - # this allows the planner to produce plans that duplicate actions - # this feature is currently on a hiatus - return_parent = 0 + closedlist = [] debug("[plan] solve %s starting from %s", goal, start_action) debug("[plan] memory supplied is %s", start_memory) @@ -114,24 +97,21 @@ def plan(caller, actions, start_action, start_memory, goal): success = False while openlist or pushback: - # get the best node. + # get the best node and remove it from the openlist if pushback is None: keyNode = heappop(openlist)[1] else: keyNode = heappushpop(openlist, (pushback.g + pushback.h, pushback))[1] pushback = None - - #debug("[plan] testing %s against %s", keyNode.action, keyNode.memory) - + # if our goal is satisfied, then stop - #if (goal.satisfied(keyNode.memory)) and (return_parent == 0): if goal.test(keyNode.memory): success = True debug("[plan] successful %s", keyNode.action) break - for child in get_children(caller, keyNode, actions, return_parent): + for child in get_children(parent, keyNode, builders): if child in openlist: possG = keyNode.g + child.cost if (possG < child.g): @@ -140,21 +120,19 @@ def plan(caller, actions, start_action, start_memory, goal): # TODO: update the h score else: # add node to our openlist, using pushpack if needed - if pushback == None: + if pushback is None: heappush(openlist, (child.g + child.h, child)) else: heappush(openlist, (pushback.g + pushback.h, pushback)) - pushpack = child - - return_parent = 0 + pushback = child if success: - path0 = [keyNode.action] - while not keyNode.parent is None: + path = [keyNode.action] + while keyNode.parent is not None: + path.append(keyNode.action) keyNode = keyNode.parent - path0.append(keyNode.action) - return path0 + return path else: return [] diff --git a/test.py b/test.py index 6d589ac..945472a 100644 --- a/test.py +++ b/test.py @@ -9,7 +9,7 @@ ...any way he knows how. """ -__version__ = ".010" +__version__ = ".012" from pygoap.agent import GoapAgent from pygoap.environment import ObjectBase @@ -82,6 +82,7 @@ def run_once(): pirate = Human("Male", "jack") load_commands(pirate, os.path.join("npc", "pirate")) pirate.add_goal(SimpleGoal(is_drunk=True)) + pirate.add_goal(SimpleGoal(aware=True)) formosa.add(pirate) formosa.set_position(pirate, (formosa, (0,0)))