mirror of
https://github.com/worldforge/cyphesis
synced 2026-08-13 12:26:04 -04:00
206 lines
8 KiB
Python
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
|