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

2024-04-03 23:28

本文主要是介绍基于约束求解器对“火影忍者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_modelm = 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_statusif 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

这篇关于基于约束求解器对“火影忍者Online”进行智能布阵的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/874249

相关文章

SQL中的外键约束

外键约束用于表示两张表中的指标连接关系。外键约束的作用主要有以下三点: 1.确保子表中的某个字段(外键)只能引用父表中的有效记录2.主表中的列被删除时,子表中的关联列也会被删除3.主表中的列更新时,子表中的关联元素也会被更新 子表中的元素指向主表 以下是一个外键约束的实例展示

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

hdu 1166 敌兵布阵(树状数组 or 线段树)

题意是求一个线段的和,在线段上可以进行加减的修改。 树状数组的模板题。 代码: #include <stdio.h>#include <string.h>const int maxn = 50000 + 1;int c[maxn];int n;int lowbit(int x){return x & -x;}void add(int x, int num){while

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

poj 3159 (spfa差分约束最短路) poj 1201

poj 3159: 题意: 每次给出b比a多不多于c个糖果,求n最多比1多多少个糖果。 解析: 差分约束。 这个博客讲差分约束讲的比较好: http://www.cnblogs.com/void/archive/2011/08/26/2153928.html 套个spfa。 代码: #include <iostream>#include <cstdio>#i

poj 3169 spfa 差分约束

题意: 给n只牛,这些牛有些关系。 ml个关系:fr 与 to 牛间的距离要小于等于 cost。 md个关系:fr 与 to 牛间的距离要大于等于 cost。 隐含关系: d[ i ] <= d[ i + 1 ] 解析: 用以上关系建图,求1-n间最短路即可。 新学了一种建图的方法。。。。。。 代码: #include <iostream>#include

智能交通(二)——Spinger特刊推荐

特刊征稿 01  期刊名称: Autonomous Intelligent Systems  特刊名称: Understanding the Policy Shift  with the Digital Twins in Smart  Transportation and Mobility 截止时间: 开放提交:2024年1月20日 提交截止日

基于 YOLOv5 的积水检测系统:打造高效智能的智慧城市应用

在城市发展中,积水问题日益严重,特别是在大雨过后,积水往往会影响交通甚至威胁人们的安全。通过现代计算机视觉技术,我们能够智能化地检测和识别积水区域,减少潜在危险。本文将介绍如何使用 YOLOv5 和 PyQt5 搭建一个积水检测系统,结合深度学习和直观的图形界面,为用户提供高效的解决方案。 源码地址: PyQt5+YoloV5 实现积水检测系统 预览: 项目背景