基于约束求解器对“火影忍者Online”进行智能布阵

文章目录

  • 1. 游戏背景
  • 2. 确定决策边界
  • 3. 布阵数据
    • 3.1 追击状态
    • 3.2 角色信息
    • 3.3 个性化要求
  • 4. 智能布阵模型
    • 4.1 主要的决策变量
    • 4.2 约束条件(含辅助决策变量)
    • 4.3 目标函数及求解


1. 游戏背景

今天将以“火影忍者Online”为案例,写一个智能布阵的脚本。我最早差不多是在十年前接触到这个游戏,相比于普通的回合制游戏,他里面有一个特别的机制,叫做 “追击”,简单来说,普通的回合制是你一下我一下,而“追击”是在一定的条件下会触发的额外攻击。例如下图的右侧说明,当某个角色进行攻击后,造成了“大浮空”的状态,当这个状态符合另一个角色追击的触发条件时,另一个角色将会进行追击。这就完成了一次追击,当触发的状态链路越长,则说明额外的攻击次数越多。

在这里插入图片描述

尽管最终对局输赢的因素非常多且复杂,但许多玩家包括我在内,希望自己的搭配出的阵容,能在每个回合内有一个较高的追击次数(期望),在同等战力水平下,这个目标确实会提高对局的赢面。而为了达到这个目标,依稀记得当年我常常在本子上记下各个角色的追击信息(触发条件,和造成的条件),并尝试不同的搭配,以及参考其他玩家的阵容搭配。

从专业一点的角度而言,尝试不同的阵容搭配,是属于启发式构造和仿真验证的方法,即我不确定这几个角色的追击能不能串起来,我要么进行不同的组合尝试,要么基于核心的角色进行阵容扩展;而参考其他玩家的阵容的方法,就有点像是元启发式方法中的群体智能,即每个玩家都是一个智能体,当有些玩家开发出强力的阵容时,且该阵容越强,大家模仿的速度越快,如果不能经常更新角色库,或者刷新战力系统(群体的多样性弱),那么最后大家都会稳定地趋于这些个阵容。

但是,随着游戏的发展,角色池越来越深,自主搭配往往不能实现过多的搭配方案,而越来越多样的角色特性和战力体系,使得大家在后期的阵容百花齐放,对这些方案的验证以及模仿具有较大的滞后性。而本文打算将该角色布阵问题视为一个组合优化问题,并用约束求解器CP-SAT进行求解。

2. 确定决策边界

这个布阵问题很典型的是 NP-Hard 问题,相比于典型的车辆路径问题,还多了很多特定的因素,例如,相同的追击路线(从一个状态到另一个状态)可能存在多个、并非所有忍者都会追击、以及追击路线可以出现多个子环路、追击路线并不需要回到初始的触发状态等等。这些特定因素使得这个问题比路径问题的问题规模更复杂,但同样的,这个问题在确定一个方案之后,却能在多项式时间内准确返回出方案的最长追击路线,但无法确定该追击路线就是最优的。

这里,我们对布阵问题的决策边界进行说明,智能布阵问题进行了一定的简化后,主要的决策内容包括:

  1. 角色选择:在一个阵容当中,只能包含 4 4 4 名忍者,其中一个是不可或缺的核心忍者,暂且称之为“主角”,另外 3 3 3 个是从角色池当中挑选的辅助忍者,除了主角,这 3 3 3 个辅助忍者的选择,是一个决策点;
  2. 主角能力选择:辅助忍者的能力(攻击、奥义、追击)都是固定的,而主角的能力可以在若干组选择在确定,这是关于主角能力的选择决策,合适的选择能够与辅助忍者搭配出较好的追击方案。

本文中的决策目标是如何选择主角能力,搭配不同的辅助忍者,使得阵容 每回合的期望追击数最大

注意:这里仅仅是以追击数作为目标,这个目标并不总能决定对局的输赢。

3. 布阵数据

建立布阵模型需要用到的基础数据如下。

3.1 追击状态

这个追击状态既可以是触发追击的状态,也可以是追击造成的状态。本文中只考虑如下的 4 4 4 种追击状态:

all_status = ["倒地", "大浮空", "击退", "小浮空"]

在原游戏当中,还有一种能触发追击的特殊状态,叫 “xx连击”,例如 “十连击”,意思是某一方的阵容,连续击打另一方 10 10 10 次及以上时,会触发一次 “十连击” 的追击。之所以不考虑该状态,最最主要的原因还是难以预估,首先,连击可以是一个角色的攻击或者技能造成连击(连击是有概率的,且连击率受角色特殊技能的影响会发生变化),其次如果连击是由一方多个角色完成,那就要考虑对战双方的 “先攻” 属性,例如己方全体先攻属性碾压另一方,那么只有等到己方所有角色攻击结束,对方才会开始行动。再次,连击还跟被攻击方的人数有关,例如,同样释放一个全体技能(单次打击),如果对方 4 4 4 个人,那么会累计 4 4 4 次攻击,如果对方 1 1 1 个人,那么会累计 1 1 1 次攻击。最后,是由于这种追击不会触发新的可被追击的状态,顶多会触发新的连击,因此,即使有,也是作为收尾动作,这使得少考虑该状态并不影响主体的追击路线。

3.2 角色信息

(1)可选择的角色池

接着是必须有待选择的角色池,需包含每个角色攻击、技能会触发的可被追击的状态,以及每个角色的可触发的追击弧,和可触发的次数。这里我们只罗列了 9 9 9 个角色,如下所示:

character_pool = {
      "我爱罗": {"skill": "倒地", "attack": "大浮空", "chase": {"大浮空": ["倒地", 1]}},
      "凯": {"skill": "击退", "attack": "大浮空", "chase": {"大浮空": ["小浮空", 1]}},
      "佩恩-修罗道": {"skill": "", "attack": "大浮空", "chase": {"大浮空": ["击退", 2], "小浮空": ["大浮空", 1]}},
      "干柿鬼鲛": {"skill": "倒地", "attack": "小浮空", "chase": {"击退": ["小浮空", 1]}},
      "金角-尾兽状态": {"skill": "", "attack": "小浮空", "chase": {"小浮空": ["击退", 1]}},
      "宇智波带土-暴走": {"skill": "", "attack": "倒地", "chase": {"小浮空": ["倒地", 1]}},
      "阿斯玛": {"skill": "倒地", "attack": "击退", "chase": {"击退": ["倒地", 1]}},
      "佩恩-人间道": {"skill": "倒地", "attack": "小浮空", "chase": {"击退": ["倒地", 1]}},
      "西瓜山河豚鬼-秽土转生": {"skill": "击退", "attack": "小浮空", "chase": {"小浮空": ["大浮空", 2]}}
}

其中,skill 键的值为技能会触发的状态,attack 键的值为普通攻击会触发的状态,chase 表示追击路线,每个追击的起始状态为键,结束状态在值当中,而数字表示相应追击路线的可触发次数。

(2)主角的可配置项

前面提到,在阵容当中,主角必须在场,但区别于其他的角色,主角有专属的一系列攻击、技能和追击,因此决策的一块重点是要考虑主角的可配置项,可配置项包括:技能、攻击、追击、通灵兽(额外追击)。以雷属性的主角为例,可配置项如下:

central_character = {"skill": {"封雷斩": "", "千鸟刀": "倒地", "雷遁铠甲": ""},
                     "attack": {"体术攻击": "击退", "一闪": "倒地", "雷光暗杀剑": "大浮空"},
                     "chase": {"居合斩": {"击退": ["倒地", 1]}, 
                               "千鸟锐枪": {"大浮空": ["小浮空", 1]},
                               "雷光奈落剑": {"倒地": ["小浮空", 1]},
                               "弦月斩": {"小浮空": ["击退", 1]}},
                     "pet": {"镰鼬": {"击退": ["小浮空", 1]},
                             "地狱犬": {"小浮空": ["击退", 1]},
                             "幻术鸦": {"大浮空": ["小浮空", 1]},
                             "白虎": {"大浮空": ["击退", 1]},
                             "忍猿": {"大浮空": ["倒地", 1]},
                             "小蛞蝓": {"倒地": ["小浮空", 1]},
                             "蛤蟆忠": {"击退": ["倒地", 1]},
                             "鲛鲨": {"小浮空": ["大浮空", 1]}}}

根据不同的主角,配置不同的可配置项,以及配置可以解锁使用的通灵兽。

3.3 个性化要求

由于一些辅助角色非常强势,因此有些人在布阵时希望阵容必须包含这些角色,其他的角色再围绕主角和强势角色进行扩展。这里可以进行如下的配置:

strong_character = ["宇智波带土-暴走"]
assert len(strong_character) <= 3, "阵容角色不超过 4 位!"

由于 strong_character 中的角色是必上场角色,因此增加了一个判断是,除了主角外,必上场角色数量不能超过 3 3 3 个。

此外,火影 O L OL OL 还有一个设定是,相同角色的不同系列不能同时上场,例如:“我爱罗”和“我爱罗[五代风影]” 就是同一个源体的不同系列的忍者,不能同时出现到一个阵容当中,如下配置一个 not_both_appear 变量:

not_both_appear = []

如果不能同时在阵容上的角色并不都同时出现在角色池中,not_both_appear可以不用设置。

4. 智能布阵模型

整体的思路很简单,就是把决策点设为决策变量,最后获得一个包含决策变量的目标函数(阵容单回合期望追击数最多),然后优化该目标函数返回决策变量的值,在建模时,由于变量的值相当于是未知的,此时模型需要囊括所有的决策空间。

首先建立模型框架:

from ortools.sat.python import cp_model

m = cp_model.CpModel()

4.1 主要的决策变量

通过前面在决策边界以及数据准备的介绍,大家也都知道主要的决策变量有哪些,包括:辅助角色的选择、主角技能、攻击、追击、通灵兽的选择,具体如下:

select_character = {character: m.NewBoolVar(name=character) for character in character_pool}
select_skil = {skill_name: m.NewBoolVar(name=skill_name) for skill_name in central_character["skill"]}
select_attack = {attack_name: m.NewBoolVar(name=attack_name) for attack_name in central_character["attack"]}
select_chase = {chase_name: m.NewBoolVar(name=chase_name) for chase_name in central_character["chase"]}
select_pet = {pet_name: m.NewBoolVar(name=pet_name) for pet_name in central_character["pet"]}

这些是主要的决策变量,而在添加约束的过程当中,会增加其他的辅助决策变量,之所以叫辅助决策变量,因为它们的值在主要决策变量确定之后也是确定的,但最终的目标函数与这些主要决策变量的关系又是非线性的,因此需要增加这些辅助变量作为过渡的桥梁。

4.2 约束条件(含辅助决策变量)

约束条件是对决策变量之间关系的限制,前面提到,为了使得从主要决策变量到目标函数的形式是线性的,需要加入一些辅助变量,而这些辅助变量和主要决策变量之间的关系也需要通过约束来进行限制。

(1)角色选择的硬约束

角色选择的硬性限制包括,前面提到的强力角色必定上场,且需要从角色池当中选出 3 3 3 位角色,同时,如果有同源的角色,则他们不能同时上场,约束如下:

for character in strong_character:
      m.Add(select_character[character] == 1)
m.Add(sum(select_character[_] for _ in select_character) == 3)

for pair in not_both_appear:
      m.Add(sum(select_character[character] for character in pair) <= 1)

对于主角而言,每个可配置项都需要进行一次选择,代码如下:

m.Add(sum(select_skill[_] for _ in select_skill) == 1)
m.Add(sum(select_attack[_] for _ in select_attack) == 1)
m.Add(sum(select_chase[_] for _ in select_chase) == 1)
m.Add(sum(select_pet[_] for _ in select_pet) == 1)

(2)方案的追击弧数

当前面的主决策变量定义好之后,我们获得了一个“布阵方案”,当然在还没到最后求解之前,这个方案是未知的,但在模型当中,我们可以把中间的辅助变量都写成确定的约束关系,这样,当决策变量的值发生变化时,辅助变量也会联动发生变化,就有点类似于最终的结果取决于决策变量的值固定之后,因此我在后文有时会说成,基于给定的方案怎么怎么样,但这个“给定的方案”到最后才能确定。

这里基于一个给定的方案,我们就能知道每个状态到另一个状态的追击弧数量 chase_path_num ,包括辅助角色的追击弧,主角的追击弧,主角的通灵兽的追击弧,这能方便后续计算追击路线的长度。如下:

chase_path_num = {(status1, status2): 0 for status1 in all_status for status2 in all_status
                  if status1 != status2}
for character in character_pool:
    chase_info = character_pool[character]["chase"]
    for status1 in chase_info:
        status2 = chase_info[status1][0]
        chase_path_num[status1, status2] += chase_info[status1][1] * select_character[character]
for chase_name in central_character["chase"]:
    chase_info = central_character["chase"][chase_name]
    for status1 in chase_info:
        status2 = chase_info[status1][0]
        chase_path_num[status1, status2] += chase_info[status1][1] * select_chase[chase_name]
for pet_name in central_character["pet"]:
    chase_info = central_character["pet"][pet_name]
    for status1 in chase_info:
        status2 = chase_info[status1][0]
        chase_path_num[status1, status2] += chase_info[status1][1] * select_pet[pet_name]

(3)状态是否能直接转移

显然地,如果两个状态之间的追击弧数至少为 1 1 1,则说明这两个状态是能直接进行转移的,设立相应的辅助变量 direct_chase,约束如下:

direct_chase = {}
for status1 in all_status:
    for status2 in all_status:
        if status1 != status2:
            direct_chase[status1, status2] = m.NewBoolVar(name=f"direct_chase_{status1}_{status2}")
            m.Add(chase_path_num[status1, status2] >= 1).OnlyEnforceIf(direct_chase[status1, status2])
            m.Add(chase_path_num[status1, status2] == 0).OnlyEnforceIf(direct_chase[status1, status2].Not())

(4)方案的追击弧左右两端的状态数量

来看一个简单的例子:假如现有的追击弧有 ( a → b ) ( a → c ) ( c → b ) ( b → a ) ( d → e ) (a\rightarrow b)(a\rightarrow c)(c\rightarrow b)(b\rightarrow a)(d\rightarrow e) (ab)(ac)(cb)(ba)(de),那么从 a a a 出发,最长的能触发的追击路线为 a → c → b → a a\rightarrow c\rightarrow b\rightarrow a acba,显然,拼接两个弧的需要消耗左( l l l)右( r r r)两端各 1 1 1 个状态,且起始状态固定,因此当某状态(i)能直接或间接去往另一些状态( r e l a t e d _ s t a t u s i related\_status_i related_statusi ⊂ \subset a l l _ s t a t u s all\_status all_status)时,从该状态出发可以触发的追击路线最长长度为:

l i = min ⁡ ( l i − 1 , r i ) + ∑ j min ⁡ ( l j , r j ) ∀ j ∈ r e l a t e d _ s t a t u s i l_i=\min(l_i-1, r_i)+\sum_j\min(l_j, r_j) \quad\forall j \in related\_status_i li=min(li1,ri)+jmin(lj,rj)jrelated_statusi

带入上面的例子,则可以得到 l a = 2 , r a = 1 , l b = 1 , r b = 2 , l c = 1 , r c = 1 l_a=2,r_a=1,l_b=1,r_b=2,l_c=1,r_c=1 la=2,ra=1,lb=1,rb=2,lc=1,rc=1,求解得到结果为 3 3 3,与实际相符。

这里先解决如何在模型中写出上面的式子。这个式子是典型的非线性约束,包含不止一个求最小值的操作,因此需要增加一些辅助变量,这里就有一个问题,这个辅助变量表示的是某一状态在弧集当中左右两端的数量,那此时还不知道以哪一个状态为起始点,就需要每一个状态都计算两个变量值(作为起始状态时的最小值,不作为起始状态的最小值)。当然具体的实现方式有多种,这里我增加了一个判断变量 left_is_min,即如果某一状态的左端点数小于等于右端点数,那么该状态作为起始状态时,追击路线就需要减 1 1 1,如下式子:
l i = ∑ j min ⁡ ( l j , r j ) − 1 ∀ j ∈ r e l a t e d _ s t a t u s i ∪ i , if start with i and  l i ≤ r i l_i=\sum_j \min(l_j, r_j)-1 \quad\forall j \in related\_status_i\cup{i}, \text{if start with i and $l_i\leq r_i$} li=jmin(lj,rj)1jrelated_statusii,if start with i and liri

具体实现代码如下:

status_side_num = {status: [sum(chase_path_num[status, status2] for status2 in all_status if status != status2),
                            sum(chase_path_num[status2, status] for status2 in all_status if status != status2)]
                   for status in all_status}
status_min_side_num = {}
left_is_min = {}
for status in all_status:
    status_min_side_num[status] = m.NewIntVar(lb=0, ub=100, name="min_pair_num")
    m.AddMinEquality(status_min_side_num[status], status_side_num[status])
    left_is_min[status] = m.NewBoolVar(name="status_left_is_min")
    m.Add(status_side_num[status][0] <= status_side_num[status][1]).OnlyEnforceIf(left_is_min[status])
    m.Add(status_side_num[status][0] > status_side_num[status][1]).OnlyEnforceIf(left_is_min[status].Not())

(5)状态是否相关联(直接转移+间接转移

在第( 4 4 4)点中,提到了状态的间接到达状态,举个例子,当有这么些追击弧 ( a → b ) ( c → d ) ( d → a ) (a\rightarrow b)(c\rightarrow d)(d\rightarrow a) (ab)(cd)(da),此时由于 a a a 状态不能直接或间接地到达 c , d c,d c,d 因此 c → d → a c\rightarrow d\rightarrow a cda 这一段路线并不能出现在由 a a a 作为起始状态的追击路线当中。

判断一个状态能否间接转移到另一个状态,只需要判断这两个状态之间是否存在联通的追击弧,只要存在任意一条,则认为这两个状态是间接关联的;反之,则不存在间接关联。

本文的案例中,总的状态数为 4 4 4,因此某一状态间接地到另一状态之间可以隔着 1 1 1 种或者 2 2 2 种状态。显然地,当两个状态能直接转移,说明两个状态一定是相关联的;而只要找到一条通路,则认为相关联,否则设为不关联。这里我用了一个额外的辅助变量来表示每一条通路是否可行。具体实现如下:

be_related = {}
one_status_connect = {}
two_status_connect = {}
for status1 in all_status:
    for status2 in all_status:
        if status1 != status2:
            be_related[status1, status2] = m.NewBoolVar(name='related')
            m.Add(be_related[status1, status2] == 1).OnlyEnforceIf(direct_chase[status1, status2])
            for status3 in all_status:
                if (status3 != status1) and (status3 != status2):
                    one_status_connect[status1,status3,status2] = m.NewBoolVar(name=f"from {status1} to {status2}")
                    m.Add(one_status_connect[status1,status3,status2] == 1).OnlyEnforceIf([direct_chase[status1, status3],
                                                                                           direct_chase[status3, status2]])
                    for status4 in all_status:
                        if (status4 != status3) and (status4 != status2) and (status4 != status):
                            two_status_connect[status1,status3,status4,status2] = m.NewBoolVar(name=f"from {status1} to {status2}")
                            m.Add(two_status_connect[status1,status3,status4,status2] == 1).OnlyEnforceIf([direct_chase[status1, status3],
                                                                                                           direct_chase[status3, status4],
                                                                                                           direct_chase[status4, status2]])
            m.Add(sum(one_status_connect[_] for _ in one_status_connect if ((_[0] == status1) and (_[2] == status2)))
                  + sum(two_status_connect[_] for _ in two_status_connect if ((_[0] == status1) and (_[3] == status2)))               
                  >= 1).OnlyEnforceIf(be_related[status1, status2])
            m.Add(sum(one_status_connect[_] for _ in one_status_connect if ((_[0] == status1) and (_[2] == status2)))
                  + sum(two_status_connect[_] for _ in two_status_connect if ((_[0] == status1) and (_[3] == status2)))               
                  == 0).OnlyEnforceIf(be_related[status1, status2].Not())

为什么这里说间隔的是状态是 1 1 1 种或 2 2 2 种,而不是 1 1 1 个或者 2 2 2 个呢?有两种情况:

  1. 起始状态 → \rightarrow 目标状态之间,还隔着起始状态。例如: a → b → a → c a\rightarrow b\rightarrow a\rightarrow c abac,此时 a a a c c c 之间隔着 a a a,两者一定相关联;如果是 a → b → a → c → d a\rightarrow b\rightarrow a\rightarrow c \rightarrow d abacd,则把起始状态由第一个 a a a 移到第二个 a a a,也一定是间接关联的;
  2. 起始状态 → \rightarrow 目标状态之间,隔着多个非起始状态。例如: a → b → c → b → a a\rightarrow b\rightarrow c\rightarrow b\rightarrow a abcba,同理,该情况被 a → b → a a\rightarrow b\rightarrow a aba 覆盖。

因此,只需要考虑两个状态之间的与两端不同的状态类型,而无需考虑间隔的状态数量。

(6)计算从不同状态开始的最长追击数

根据上面的公式,只遍历与起始状态有关的状态,然后加上其最小的端点数。但是在方案最终确定前,并不知道某个状态会和另外的状态是什么关系,因此需要再增加一个中间变量 add_chase_num,如果与状态相关,该变量值等于该状态的最小端点数,如果无关,则该变量值为 0 0 0;最后计算从不同状态出发的最长追击数(含决策变量的表达式)。

status_chase_num = {}
add_chase_num = {}
for status in all_status:
    status_chase_num[status] = m.NewIntVar(lb=0, ub=100, name="chase_length")
    for status2 in all_status:
        if status2 != status:
            add_chase_num[status, status2] = m.NewIntVar(lb=0, ub=100, name="chase_length")
            m.Add(add_chase_num[status, status2] == status_min_side_num[status2]).OnlyEnforceIf(be_related[status, status2])
            m.Add(add_chase_num[status, status2] == 0).OnlyEnforceIf(be_related[status, status2].Not())
    m.Add(status_chase_num[status] == status_min_side_num[status] + sum(add_chase_num[_] for _ in add_chase_num if _[0] == status)).OnlyEnforceIf(left_is_min[status])
    m.Add(status_chase_num[status] == status_min_side_num[status] + sum(add_chase_num[_] for _ in add_chase_num if _[0] == status) + 1).OnlyEnforceIf(left_is_min[status].Not())

4.3 目标函数及求解

在目标设置上,是希望阵容的单回合内出发的追击数最多,且每个角色的普通攻击或者技能都会造成特定的状态,这些造成的状态可能会有重合,因此只算一次。例如,角色 A A A 和角色 B B B 的技能都会造成“倒地”,只累计一次由“倒地”起始的最长追击次数。

另外还有一个点是,不同的起始状态的追击路线有重叠,重叠的部分应该计算多少次的问题。来看一个例子, a → b → c a\rightarrow b\rightarrow c abc,与 b → c b\rightarrow c bc,当先触发了后者,前者则只会追击到 b b b 就停下,比较自然的思路是,把相联系的状态打包在一起,只计算其中最长的追击路线即可。但是结合实际情况,不同的状态的造成概率不确定,以及每个角色的先后手顺序不确定,还有可能是某个角色还没起手,就被击败等等。这些不确定使得重复计算追击路线并不会改变对阵容追击频率的估计,因此这里我们不对重叠部分做处理,对起始于每个状态的最长追击数进行累加作为目标。

此外还需对现有进行判断,如果所选阵容的角色能够造成某个状态,才累计该状态的最长追击数,否则累计 0 0 0,需要额外增加辅助变量,具体代码如下:

create_status = {status: 0 for status in all_status}
is_from_status  = {}
add_obj = {}
for character in character_pool:
    skill_to_status = character_pool[character]["skill"]
    if skill_to_status:
        create_status[skill_to_status] += select_character[character]
    attack_to_status = character_pool[character]["attack"]
    if attack_to_status:
        create_status[attack_to_status] += select_character[character]
for skill_name in central_character["skill"]:
    if central_character["skill"][skill_name]:
        create_status[central_character["skill"][skill_name]] += select_skill[skill_name]
for attack_name in central_character["attack"]:
    if central_character["attack"][attack_name]:
        create_status[central_character["attack"][attack_name]] += select_attack[attack_name]
for status in all_status:
    is_from_status[status] = m.NewBoolVar(name="is_from_status")
    m.Add(create_status[status] >= 1).OnlyEnforceIf(is_from_status[status])
    m.Add(create_status[status] == 0).OnlyEnforceIf(is_from_status[status].Not())

    add_obj[status] = m.NewIntVar(lb=0, ub=100, name="add_boj_by_status")
    m.Add(add_obj[status] == status_chase_num[status]).OnlyEnforceIf(is_from_status[status])
    m.Add(add_obj[status] == 0).OnlyEnforceIf(is_from_status[status].Not())
    
m.Maximize(sum(add_obj[_] for _ in add_obj))

通过一下代码求解并打印相关信息:

solver = cp_model.CpSolver()
status = solver.Solve(model=m)
print("求解结果", status, solver.StatusName())

if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:

    print(f'目标函数值: {solver.ObjectiveValue()}\n')

    print("选择的忍者:", end=" ")
    for character in character_pool:
        if solver.Value(select_character[character]) == 1:
            print(f"{character}", end=" ")
    print()
    for skill_name in central_character["skill"]:
        if solver.Value(select_skill[skill_name]) == 1:
            print("主角技能: ", skill_name, central_character["skill"][skill_name])
    for attack in central_character["attack"]:
        if solver.Value(select_attack[attack]) == 1:
            print("主角攻击: ", attack, central_character["attack"][attack])
    for chase in central_character["chase"]:
        if solver.Value(select_chase[chase]) == 1:
            print("主角追击: ", chase, central_character["chase"][chase])
    for pet in central_character["pet"]:
        if solver.Value(select_pet[pet]) == 1:
            print("主角通灵兽: ", pet, central_character["pet"][pet])
    print()
    for status in all_status:
        print(f"从 {status} 开始的追击数:{solver.Value(add_obj[status])}")
else: 
	print('No solution found.')

案例求解结果:

求解结果 4 OPTIMAL
目标函数值: 25.0

选择的忍者: 佩恩-修罗道 宇智波带土-暴走 佩恩-人间道
主角技能:  封雷斩
主角攻击:  体术攻击 击退
主角追击:  雷光奈落剑 {'倒地': ['小浮空', 1]}
主角通灵兽:  镰鼬 {'击退': ['小浮空', 1]}

从 倒地 开始的追击数:6
从 大浮空 开始的追击数:7
从 击退 开始的追击数:6
从 小浮空 开始的追击数:6

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/515073.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32工程 如何设置堆栈大小(Heap和Stack)

方法1&#xff1a;通过CubeMX、CubeIDE 配置 方法2&#xff1a;直接在启动文件中修改 &#xff08;适合所有Keil工程&#xff09; Heap、Stack的值大小&#xff0c;不管使用哪种开发环境&#xff0c;它俩都肯定在启动文件中。 可以通过CtrlF&#xff0c;搜索: Heap&#xff0…

【Linux】从零认识文件操作

送给大家一句话&#xff1a; 要相信&#xff0c;所有的不美好都是为了迎接美好&#xff0c;所有的困难都会为努力让道。 —— 简蔓《巧克力色微凉青春》 开始理解基础 IO 吧&#xff01; 1 前言2 知识回顾3 理解文件3.1 进程和文件的关系3.2 文件的系统调用openwrite文件 fd 值…

STL常用容器(2)---vector容器

1.1 vector基本概念 功能&#xff1a; vector数据结构和数组非常相似&#xff0c;也称为单端数组 vector与普通数组区别&#xff1a; 不同之处在于数组是静态空间&#xff0c;而vector可以动态扩展 动态扩展&#xff1a; 并不是在原空间之后的续接的新空间&#xff0c;而…

如何从 Android 和 iPhone 中的 SIM 卡恢复已删除的联系人 [新]

在手机上&#xff0c;我们经常添加联系人&#xff0c;而很少关心联系人是存储在SIM卡中还是手机中。当我们错误删除SIM卡联系人&#xff0c;或者不当取出插入的SIM卡插入新手机时&#xff0c;那些因业务需要而添加的联系人就会消失。这可能会令人沮丧和困惑。因此&#xff0c;您…

UniApp 应用发布到苹果商店指南

&#x1f680; 想要让你的 UniApp 应用在苹果商店亮相吗&#xff1f;别着急&#xff0c;让我来带你一步步完成这个重要的任务吧&#xff01;在这篇博客中&#xff0c;我将详细介绍如何将 UniApp 应用顺利发布到苹果商店&#xff0c;让你的应用跻身于苹果生态之中。 引言 &…

Python向带有SSL/TSL认证服务器发送网络请求小实践(附并发http请求实现asyncio+aiohttp)

1. 写在前面 最近工作中遇到这样的一个场景&#xff1a;给客户发送文件的时候&#xff0c;为保证整个过程中&#xff0c;文件不会被篡改&#xff0c;需要在发送文件之间&#xff0c; 对发送的文件进行签名&#xff0c; 而整个签名系统是另外一个团队做的&#xff0c; 提供了一…

银行数字化转型导师坚鹏:银行数字化转型必知的3大客户分析维度

银行数字化转型需要进行客户分析&#xff0c;如何进行客户分析呢&#xff1f;银行数字化转型导师坚鹏认为至少从客户需求分析、客户画像分析、客户购买行为分析3个维度进行客户分析。 1.客户需求分析 银行数字化转型需要了解客户需求&#xff0c;不同年龄段的客户有不同的需求…

游戏APP如何提高广告变现收益的同时,保证用户留存率?

APP广告变现对接第三方聚合广告平台主要通过SDK文档对接&#xff0c;一些媒体APP不具备专业运营广告变现的对接能力和资源沉淀&#xff0c;导致APP被封控&#xff0c;设置列入黑名单&#xff0c;借助第三方聚合广告平台进行商业化变现是最佳选择。#APP广告变现# 接入第三方平台…

VGG网络模型

VGG网络模型 VGG的网络架构VGG16VGG19 特点总结时间关系AlexNet和VGG相似之处AlexNet和VGG不同之处启发与影响总结 VGG&#xff08;Visual Geometry Group&#xff09;是由牛津大学的 Visual Geometry Group 提出的一个深度卷积神经网络模型&#xff0c;它在2014年的ImageNet大…

哲♂学家带你深♂入了解动态顺序表

前言&#xff1a; 最近本哲♂学家学习了顺序表&#xff0c;下面我给大家分享一下关于顺序表的知识。 一、什么是顺序表 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;一般情况下采用数组存储。在数组 上完成数据的增删查改。 顺序表&#xff…

动态规划刷题(算法竞赛、蓝桥杯)--乌龟棋(线性DP)

1、题目链接&#xff1a;[NOIP2010 提高组] 乌龟棋 - 洛谷 #include <bits/stdc.h> using namespace std; const int M41; int f[M][M][M][M],num[351],g[5],n,m,x; //f[a][b][c][d]表示放a个1b个2c个3d个4的总得分 int main(){scanf("%d %d",&n,&m)…

创新指南|贝恩的产品经理RAPID框架:解决问题的分步指南,使决策过程既高效又民主

您是否曾发现自己陷入项目的阵痛之中&#xff0c;决策混乱、角色不明确、团队成员之间的冲突不断升级&#xff1f;作为产品经理&#xff0c;驾驭这艘船穿过如此汹涌的水域可能是令人畏惧的。应对这些挑战的关键在于采用清晰、结构化的决策方法。输入贝恩的 RAPID 框架&#xff…

软件测试用例(2)

具体的设计方法 -- 黑盒测试 因果图 因果图是一种简化的逻辑图, 能直观地表明程序的输入条件(原因)和输出动作(结果)之间的相互关系. 因果图法是借助图形来设计测试用例的一种系统方法, 特别适用于被测试程序具有多种输入条件, 程序的输出又依赖于输入条件的各种情况. 因果图…

Linux-进程概念

1. 进程基本概念 书面概念&#xff1a;程序的一个执行实例&#xff0c;正在执行的程序等 内核概念&#xff1a;担当分配系统资源&#xff08;CPU时间&#xff0c;内存&#xff09;的实体。 2. 描述和组织进程-PCB PCB&#xff08;process contral block&#xff09;&#xff0…

【讲解下如何Stable Diffusion本地部署】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

20240324-2-频繁模式FrequentPattern

频繁模式(frequent pattern) 频繁模式一般是指频繁地出现在数据集中的模式。这种频繁模式和关联规则是数据挖掘中想要挖掘的知识。我们都知道一个很有趣的故事&#xff0c;就是啤酒和尿布的故事&#xff0c; 在某些特定的情况下&#xff0c;“啤酒”与“尿布”两件看上去毫无关…

SCP 从Linux快速下载文件到Windows本地

需求&#xff1a;通过mobaxterm将大文件拖动到windows本地速度太慢。 环境&#xff1a;本地是Windows&#xff0c;安装了Git。 操作&#xff1a;进入文件夹内&#xff0c;鼠标右键&#xff0c;点击Git Bash here&#xff0c;然后输入命令即可。这样的话&#xff0c;其实自己本…

java农家乐旅游管理系统springboot+vue

实现了一个完整的农家乐系统&#xff0c;其中主要有用户表模块、关于我们模块、收藏表模块、公告信息模块、酒店预订模块、酒店信息模块、景区信息模块、景区订票模块、景点分类模块、会员等级模块、会员模块、交流论坛模块、度假村信息模块、配置文件模块、在线客服模块、关于…

基于深度学习的番茄成熟度检测系统(网页版+YOLOv8/v7/v6/v5代码+训练数据集)

摘要&#xff1a;在本博客中&#xff0c;我们深入探讨了基于YOLOv8/v7/v6/v5的番茄成熟度检测系统。核心技术基于YOLOv8&#xff0c;同时融合了YOLOv7、YOLOv6、YOLOv5的算法&#xff0c;对比了它们在性能指标上的差异。本文详细介绍了国内外在此领域的研究现状、数据集的处理方…

9.图像中值腐蚀膨胀滤波的实现

1 简介 在第七章介绍了基于三种卷积前的图像填充方式&#xff0c;并生成了3X3的图像卷积模板&#xff0c;第八章运用这种卷积模板进行了均值滤波的FPGA实现与MATLAB实现&#xff0c;验证了卷积模板生成的正确性和均值滤波算法的MATLAB算法实现。   由于均值滤波、中值滤波、腐…