我家黑贞!
1. 背景
Fate/Grand Order(非的肝不过欧的)作为索尼为了拯救自己不倒闭而开发的面向月厨的骗氪养成抽卡爆肝游戏,居然没有像隔壁《阴阳师》的自动战斗系统(看看别人现在都自带脚本了)。毕竟是懒得肝,就不妨写一个脚本来肝算了,省时省力。
懒得写新的文章了orz新的版本在这个REPO里,主要实现了完全的自动刷,包括磕苹果OWO,文中的版本在文末。
2. 思路:界面识别
本来以为搞这个的难度不会比《阴阳师》的难太多QAQ,我真的是too young too simple啊QAQ
注意:由于我个人比较懒,所以之前文章提到的就懒得再提了QWQ
所以关于通过ADB对手机进行操作和OpenCV这个库的使用也不再赘述,不过可以参考我之前写的两篇文章:
《用Python实现阴阳师自动结界突破》
《用Python实现阴阳师自动抽卡》
(抽卡那篇文章基本概括了ADB的用法,和要用到的OpenCV的函数的用法了)
2.1 开始
我们这次要做的可不是什么抽卡脚本,而是一个战斗脚本,其实可以算是AI的初步了。虽然只是暴力算出造成最大伤害的方案orz。
我们在这里不考虑释放技能、宝具和暴击星这三样非常重要的东西。。。只单纯考虑克制、抵抗和不同种类卡打出的伤害,目标就就是算出伤害最高的组合。
2.2 指令卡
战斗界面
要开始,我们首先要分析界面的组成。首先下面是一排指令卡,每张指令卡都有卡的种类(黄色框)和“克制”和“抵抗”的标记(黄色圈)之类的东西。那我们可以把每张指令卡视为一个对象,然后把它的特点抽象出来。我们可以知道每张卡都有一个坐标,一个类型(绿蓝红),一种状态(无/克制/抵抗),还有在点按是的顺序(1/2/3)和伤害系数(这个具体有一张表)。
所以我们可以这样做:
class Card:
def __init__(self):
self.crd = [] # the coordinate of the card (x, y)
self.status = 0 # the status of the card "normal(0)" "restraint(1)" "resistance(2)"
self.type = [] # type of the card "Quick(0)" "Arts(1)" "Buster(2)"
self.priority = 0 # the priority of the card
self.atk = 1 # set atk of the card
2.3 识别与匹配
2.3.1 坐标匹配
这个其实就没啥难度的了,无非就是调用OpenCV的库(之前的文章都提到过)用matchTemplate()识别图像然后返回坐标。不过我们倒是要写一个 “过滤系统”来把相近的坐标过滤掉,最后得到5张指令卡的坐标。这个简单来说,也可以用穷举法,设定一个范围,使这个范围里的坐标只保留一个。
2.3.2 标记匹配
另外一个重点就是把“克制”和“抵抗”的标记和其所在的卡匹配在一起。通过多组数据我们就可以观察到指令卡的坐标和标记的坐标的差值总在一个范围里面,简单的话就是设置一个范围如果标记的坐标在这个范围里则标记这张指令卡。
def mark_crd(card, mark):
note = []
for i in range(len(card)):
for j in range(len(mark)):
for p in range_of_x: # 两坐标x差值范围
for q in range_of_y: # 两坐标y差值范围
if (card[i][0] + p == mark[j][0]) and (card[i][1] - q == mark[j][1]): # 如果在范围内
note.append(card[I])
return note
2.3.3 种类匹配
然后我们还有给每张指令卡标记上卡的种类这个和前面匹配标记也是差不多的,就不再赘述了。
总的来说,这样就把每张卡(对象)的属性给匹配了起来,这样子就可以后面的程序调用了。
3. 思路:出卡顺序
整个算法的核心就是这一部分,计算出造成伤害最大的组合。
3.1 计算法则
就是这张图!
分析这张图,可以看到红卡放第一张时后面的卡都有伤害加成(废话),而其他颜色的卡则为原来的伤害,只是后面的卡伤害会略高而已。。。
最简单的办法就是暴力的把它写成一堆if-else语句QWQ
3.2 实现
我们在前面已经初始化了伤害(atk=1),我们只要给克制的伤害乘2,抵抗的乘0.5就好了,然后再加上我们那一大坨if-else语句
def rank_card(): # main algorithm
# 设置 atk total rank这三个数组
for i in range(5):
cards[i].priority = 1
for j in range(5):
if j == I:
continue
else:
cards[j].priority = 2
for k in range(5):
if k == i or k == j:
continue
else:
if cards[i].type == 2: # 第一张是buster的话
# 第二张的伤害 balabala
# 第三张的伤害 balabala
else:
if cards[i].type == 1: # 第一张是arts的话
atk[0] = 1 * cards[i].atk
elif cards[i].type == 0: # 第一张quick的话
atk[0] = 0.8 * cards[i].atk
# 第二张的伤害 balabala
# 第三张的伤害 balabala
if (sum(atk)) > total:
total = sum(atk)
rank[0] = I
rank[1] = j
rank[2] = k
# 返回伤害最高的卡的号码
return rank
反正人懒,给机器做多点事也没关系XD,接下来只要把坐标返回给主程序点按的可以了。
4. 思路:防封
听说FGO会封脚本,所以就特地加入了防封的机制。
其实方法很简单,加入随机的点按,和不同的间隔(等待时间)就可以了,点按每张卡有位置的变化,点每张卡之间有变化的间隔,和一些故意的“误触”应该就没问题,其实还可以加上一些长度不同的滑动也是可以的,简单来说就是一堆随机函数而已233
5. 思路:整合
简单来说就是把上面的一堆代码整合到一起就可以了
开始界面
识别到这个界面然后点按“Attack”
结束界面
识别到“与从者的羁绊”终止脚本
中间就是上面所提到的了。也即是一个不停的循环,直到“结束”界面才终止。有什么其他的就到时候再补充吧OWO
6. 总结
这应该是我搞过最大最复杂的一个项目了,也是第一次接触到一点OOP。然而这个项目还是偏实用性,毕竟没有什么高端的,或者更高效率的算法,这也应该是以后要改进的地方。
然后是惯例,代码在Github上面QWQ
还有Bilibili上的演示视频XD