如何解决Python Game Planner - 延迟函数执行的意外行为 - 范围/绑定/关闭问题?
我正在为我的回合制游戏中的敌人实施决策模式。为了帮助理解代码(我希望!),我将解释它正在实现的模式:
- 当敌人接到 take_a_turn() 指令时,他们会制定计划,对战斗中的每个有效目标(例如攻击玩家、治疗自己、治疗盟友)采取所有可用的行动。
- 每个计划都包含一个操作(函数)和一个分数(int)。
- 敌人的目标带有数值,这些数值会影响他们制定的计划的得分。例如,一个敌人可能只有伤害玩家的目标,因此永远不会使用治疗能力。另一个敌人可能有两个目标,但更喜欢伤害玩家而不是治疗。
- 制定计划后,分数会根据各种因素进行加权。例如,根据目标,敌人可能更喜欢伤害玩家而不是治疗盟友,但如果特定盟友的生命值极低,则治疗该盟友的价值可能足够高,以至于该计划暂时优先于殴打玩家。立>
- 当发现一个可行的计划时,函数(例如攻击或治疗法术)及其参数(使用它的敌人、目标、战斗状态)被分配给计划动作变量以供稍后调用,如果选择了计划。
- 一旦敌人制定了每一个可行的计划,他们就会按得分排序,并选择得分最高的计划。这是敌人可以根据他们的能力、目标和当前战斗状态在回合中做的“最好”的事情。然后执行所选计划的动作变量,导致敌人执行此操作。
这个系统运行良好。我从以前用 C# 编写的游戏中成功移植了它。当将函数分配给计划的操作时,在 C# 中,使用 lambda 来提前绑定参数(用户、目标、战斗状态)。
Python 中的一切都正常工作...除了它执行错误的操作(它似乎执行了最后计划的操作),即使它选择了正确的计划(在我的测试用例中是 3 个中的第 2 个)和选定的打印出来的计划调试文本是正确的。我相信这可能与 python 范围和与 C# 的绑定差异有关,我已经对其进行了广泛的研究并尝试了几种不同的解决方案(lambda、内联函数、部分、不同的参数构造),但它们的行为都相同。不幸的是,分配了一个函数的计划操作变量在我的 IDE (pycharm) 调试器中没有可视化,所以我无法在逐步执行时明确看到它的变化。这是相关的代码。我省略了不重要的参考文献以使其更短,但如果我跳过的任何参考文献可能有用,请发表评论,我会添加它们。
敌人,以及所有随之而来的决策逻辑:
class GoalType(Enum):
damage_player = 1
debuff_player = 2 #not used in this example
heal_ally = 3
buff_ally = 4 # not used in this example
summon = 5 # not used in this example
class Goal:
def __init__(self,goal_type: GoalType,value: int):
self.goal_type = goal_type
self.value = value
# this method looks like overkill,but several future goals have multiple contributor types
@staticmethod
def get_contributor_effects_by_goal_type(goal_type: GoalType):
if goal_type == GoalType.damage_player:
contribs = [EffectType.damage_health]
elif goal_type == GoalType.heal_ally:
contribs = [EffectType.restore_health]
else:
raise Exception(f'GoalType {goal_type} has no configured contributing effects')
return contribs
class Plan:
def __init__(self):
self.action = None
self.debug = ''
self.score = 0
class Enemy:
# I omitted all the enemy member variables here not related to the problem,for brevity.
# AI
self.actions = actions
self.goals = goals
def take_a_turn(self,fight):
plans = self.get_action_plans(fight)
if len(plans) > 0:
print(f'{self.name}\'s plans:')
for plan in plans:
print(': ' + plan.debug)
plans.sort(key=lambda x: x.score,reverse=True)
the_plan = plans[0]
print(f'The chosen plan is: --{the_plan.debug}-- w/ score {the_plan.score}')
return the_plan.action()
else:
return f'{self.name} took no action.'
def get_action_plans(self,fight):
plans = self.get_kill_player_action_plans(fight)
if len(plans) > 0:
return plans
# damage_player goal
goal = [x for x in self.goals if x.goal_type == GoalType.damage_player]
if len(goal) > 0:
plans += self.get_damage_player_plans(goal[0],fight)
# heal_ally goal
goal = [x for x in self.goals if x.goal_type == GoalType.heal_ally]
if len(goal) > 0:
plans += self.get_heal_ally_plans(goal[0],fight)
return plans
def get_damage_player_plans(self,goal,fight):
plans = []
for action in self.actions:
if action.targets_players and action.is_usable(fight.states):
effects = list(filter(lambda effect: effect.type == EffectType.damage_health,action.effects))
if len(effects) > 0:
for character in fight.characters:
dmg = character.estimate_damage_from_enemy_action(self,action)
plan = Plan()
plan.score = goal.value + int(100.0 * dmg / character.health)
plan.action = lambda: action.do(user=self,target=character,fight=fight)
plan.debug = f'damage {character.name} w/ {action.name} score {plan.score}'
plans.append(plan)
return plans
def get_heal_ally_plans(self,fight):
plans = []
for action in self.actions:
if action.targets_allies and action.is_usable(fight.states):
effects = list(filter(lambda effect: effect.type == EffectType.restore_health,action.effects))
if len(effects) > 0:
for enemy in fight.enemies:
plan = Plan()
plan.score = goal.value + 100 - int(enemy.current_health / enemy.health * 100)
plan.action = lambda: action.do(user=self,target=enemy,fight=fight)
plan.debug = f'heal {enemy.name} w/ {action.name} score {plan.score}'
plans.append(plan)
return plans
用于测试的敌人--忽略所有数字,只是各种统计数据
enemies = {
'slime': Enemy('Slime',1,0.3,10,0.1,5,0.2,0.0,[SingleTargetAttack('Headbutt','',0.05,[SpellEffect(EffectType.damage_health,Elements.earth,4)]),SingleTargetHeal('Regenerate',3,[SpellEffect(EffectType.restore_health,Elements.water,2,5)])],[Goal(GoalType.damage_player,500),Goal(GoalType.heal_ally,450)]),}
能力及其 do() 函数是如何定义的
class SingleTargetHeal(Action):
def __init__(self,name: str,description: str,cooldown: int,effects: [SpellEffect]):
for effect in effects:
if effect.type != EffectType.restore_health:
raise Exception(f'SingleTargetHeal {name} has an unsupported effect type {effect.type}')
super().__init__()
self.name = name
self.description = description
self.cooldown = cooldown
self.effects = effects
self.targets_players = False
self.targets_allies = True
self.area = 0
self.area_modifiable = False
def do(self,user,target,fight):
out = f'{user.name} used {self.name} on {target.name}.'
targets = [target]
if self.area > 0:
i = self.area
while i > 0:
if fight.enemies.index(target) + i <= len(fight.enemies) - 1:
targets.append(fight.enemies.index(target) + i)
if fight.enemies.index(target) - i > 0:
targets.insert(0,fight.enemies.index(target) - i)
i -= 1
for target in targets:
for effect in self.effects:
heal = target.restore_health(random.randint(effect.min,effect.max),user)
out += f'\n{target.name} regained {heal} health.'
return out
基类 Action 定义了肯定可以正常工作的 is_usable()。 SingleTargetAttack 动作本质上与 SingleTargetHeal 相同,但攻击玩家而不是治疗敌人。这是有效的,因为如果我移除重生法术和治疗目标,敌人只能按计划进行攻击,并且正确执行。
来自规划器的调试语句
Slime's plans:
: damage Player w/ Headbutt score 502
: heal Slime w/ Regenerate score 450
The chosen plan is: --damage justindz#4247 w/ Headbutt score 502-- w/ score 502
实际发生了什么
Pico Slime goes next.
Pico Slime used Regenerate on you.
You regained 3 health.
如您所见,当调用所选计划的操作时,敌人实际上对玩家使用了再生。啊。这应该是不可能的,因为它不是有效的计划之一,因此我怀疑它与范围/绑定相关。我错过了什么?
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。