微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Python Game Planner - 延迟函数执行的意外行为 - 范围/绑定/关闭问题?

如何解决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 举报,一经查实,本站将立刻删除。