用Python實現FGO自動戰鬥指令碼

我家黑貞!
1. 背景
Fate/Grand Order(非的肝不過歐的)作為索尼為了拯救自己不倒閉而開發的面向月廚的騙氪養成抽卡爆肝遊戲,居然沒有像隔壁《陰陽師》的自動戰鬥系統(看看別人現在都自帶指令碼了)。畢竟是懶得肝,就不妨寫一個指令碼來肝算了,省時省力。
懶得寫新的文章了orz新的版本在這個 REPO 裡,主要實現了完全的自動刷,包括磕蘋果OWO,文中的版本在文末。
2. 思路:介面識別
本來以為搞這個的難度不會比《陰陽師》的難太多QAQ,我真的是too young too simple啊QAQ
注意:由於我個人比較懶,所以之前文章提到的就懶得再提了QWQ
所以關於通過 ADB 對手機進行操作和 OpenCV 這個庫的使用也不再贅述,不過可以參考我之前寫的兩篇文章:
(抽卡那篇文章基本概括了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