cyphesis/data/rulesets/basic/scripts/mind/Goal.py
Erik Ogenvik 6c77769876 Added delayed and one shot goals.
This allows the welcome goal to be delayed.
2020-06-28 13:21:04 +02:00

206 lines
8 KiB
Python

# This file is distributed under the terms of the GNU General Public license.
# Copyright (C) 1999 Aloril (See the file COPYING for details).
import importlib
import types
from common import log
# \defgroup PythonGoals Goal Classes
# \brief Base class for all goals
# \ingroup PythonGoals
class Goal:
def __init__(self,
desc="",
fulfilled=None,
sub_goals: list = None,
validity=None,
debug=False):
""" Init
Args:
desc (string): A user facing description of the goal
fulfilled (func): A function returning a bool whether the goal is fulfilled or not.
Fulfilled goals will be skipped by the mind code when determining which goal to run next.
sub_goals (list): A list of sub goals which will be checked and executed in order.
validity (func): A function returning bool whether the goal still is valid. Used mainly by other goals.
debug (int): A flag declaring whether debug output should be enabled.
"""
if sub_goals is None:
sub_goals = []
self.desc = desc
# mind sets these:
# self.key
if fulfilled:
self.fulfilled = fulfilled
else:
self.fulfilled = lambda me: False
# If no validity function is supplied the goal is always considered valid
if validity:
self.validity = validity
else:
self.validity = lambda me: True
# filter out any None subgoals
self.sub_goals = [item for item in sub_goals if item is not None]
self.debug = debug
self.vars = []
# keeps track of whether the goal is fulfilled or not
# this is mainly of use for inspection and diagnosis
self.is_fulfilled = False
# any subgoal/function/method can set this and
# it's checked at start of check_goal_rec
# and NPCMind.py fulfill_goals uses it too to remove goals from list
self.irrelevant = False
# Tracks the number of errors this goal (or any of its subgoals) has produced.
# This is used by NPC code to remove troublesome goals from the processing
self.errors = 0
# If an error has occurred, the description of the last one is stored here.
# This is mainly of use for debugging.
self.lastError = ""
self.lastProcessedGoals = None
def __repr__(self):
return self.info()
def __str__(self):
return self.info()
def triggering_goals(self):
goals = []
for sub_goal in self.sub_goals:
if hasattr(sub_goal, "triggering_goals"):
goals.extend(sub_goal.triggering_goals())
return goals
def info(self):
name = self.__class__.__name__
if name == "Goal":
return name + "(" + repr(self.desc) + ")"
var = ""
for v in self.vars:
if var:
var = var + ","
var = var + repr(getattr(self, v))
return name + "(" + var + ")"
def is_valid(self, me):
"""Checks if this goal is valid. By this we mean whether the goal is possible to fulfill.
If no validity function was supplied at goal creation time the goal is always considered valid. """
return self.validity(me)
def check_goal(self, me):
"""executes goal. Any response that's not None causes the goal checking code to stop after this goal"""
if self.debug:
log.thinking("GOAL desc: " + str(self))
res, debug_info = self.check_goal_recursively(me, 0, "")
if len(debug_info) != 0:
# Keep a copy of the debug info for the "report" method.
self.lastProcessedGoals = debug_info
return res
def check_goal_recursively(self, me, depth, debug_info):
"""check (sub)goal recursively.
This is done by iterating over all sub goals, breaking if any sub goal returns an operation."""
res = None
if self.irrelevant:
return res, debug_info
if self.debug:
log.thinking("\t" * depth + "GOAL: bef fulfilled: " + self.desc + " " + repr(self.fulfilled))
if self.fulfilled(me):
self.is_fulfilled = 1
if self.debug:
log.thinking("\t" * depth + "GOAL: is fulfilled: " + self.desc + " " + repr(self.fulfilled))
return res, debug_info
else:
if self.debug:
log.thinking("\t" * depth + "GOAL: is not fulfilled: " + self.desc + " " + repr(self.fulfilled))
self.is_fulfilled = 0
debug_info = debug_info + "." + self.info()
# Iterate over all sub goals, but break if any goal returns an operation
for sg in self.sub_goals:
if sg is None:
continue
if isinstance(sg, types.FunctionType) or isinstance(sg, types.MethodType):
if self.debug:
log.thinking("\t" * depth + "GOAL: bef function: " + repr(sg) + " " + repr(res))
res = sg(me)
if self.debug:
log.thinking("\t" * depth + "GOAL: aft function: " + repr(sg) + " " + repr(res))
debug_info = debug_info + "." + sg.__name__ + "()"
if res is not None:
# If the function generated something, stop iterating here and return
return res, debug_info
else:
if self.debug:
log.thinking("\t" * depth + "GOAL: bef sg: " + sg.desc)
# If the subgoal is irrelevant, remove it
if sg.irrelevant:
self.sub_goals.remove(sg)
continue
res, debug_info = sg.check_goal_recursively(me, depth + 1, debug_info)
if self.debug:
log.thinking("\t" * depth + "GOAL: aft sg: " + sg.desc + ", Result: " + str(res))
# If the subgoal generated an op, stop iterating here and return
if res is not None:
return res, debug_info
return res, debug_info
def report(self):
"""provides extended information about the goal,
as well as all subgoals"""
name = self.__class__.__name__
report = {"name": name, "description": self.desc, "fulfilled": self.is_fulfilled}
if self.lastProcessedGoals:
report["lastProcessedGoals"] = self.lastProcessedGoals
sub_goals = []
for sg in self.sub_goals:
if not isinstance(sg, types.FunctionType) and not isinstance(sg, types.MethodType) and sg is not None:
sub_goals.append(sg.report())
if len(sub_goals) > 0:
report["subgoals"] = sub_goals
if len(self.vars) > 0:
variables = {}
for v in self.vars:
variables[v] = str(getattr(self, v))
report["variables"] = variables
if self.errors > 0:
report["errors"] = self.errors
report["lastError"] = self.lastError
return report
def goal_create(goal_element):
if goal_element is None:
return None
if hasattr(goal_element, 'class'):
goal_class = goal_element['class']
splits = goal_class.split('.')
module_name = '.'.join(splits[0:-1])
class_name = splits[-1]
module = importlib.import_module(module_name)
class_ = getattr(module, class_name)
params = {}
if hasattr(goal_element, 'params'):
params = goal_element['params']
print('Creating an instance of {}'.format(goal_class))
try:
instance = class_(**params)
if instance:
return instance
else:
print('Could not create goal from data\n {}'.format(str(goal_element)))
except Exception:
print('Error when creating goal from data\n {}'.format(str(goal_element)))
raise
return None