diff --git a/.gitignore b/.gitignore index 566de72..a1138e3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ m4/lt~obsolete.m4 autom4te.cache /.idea +fgobot/__pycache__ +screenshots/ diff --git a/adb.md b/adb.md new file mode 100644 index 0000000..80e3073 --- /dev/null +++ b/adb.md @@ -0,0 +1,16 @@ +# 连接 Mumu 模拟器 +[关于 如何用电脑的adb连接Mumu模拟器](https://www.cnblogs.com/hankzhouAndroid/p/9070473.html) +```powershell +adb connect 127.0.0.1:7555 +adb devices +``` + + +# 连接 Nox 模拟器 +[adb 如何连接夜神模拟器](https://blog.csdn.net/qq_36350532/article/details/79000653) + +夜神模拟器需要使用自带的 adb.exe + +```powershell +adb connect 127.0.0.1:62001 +``` diff --git a/exp_cards.png b/exp_cards.png new file mode 100644 index 0000000..e996ca8 Binary files /dev/null and b/exp_cards.png differ diff --git a/fgobot/bot.py b/fgobot/bot.py index 61b7d86..5d0f9d8 100644 --- a/fgobot/bot.py +++ b/fgobot/bot.py @@ -12,12 +12,14 @@ from time import sleep from typing import Tuple, List, Union from random import randint +import cv2 +import time logger = logging.getLogger('bot') -INTERVAL_SHORT = 1 -INTERVAL_MID = 2 -INTERVAL_LONG = 10 +INTERVAL_SHORT = 0.5 +INTERVAL_MID = 1 +INTERVAL_LONG = 5 class BattleBot: @@ -86,7 +88,7 @@ def __init__(self, self.friend_threshold = friend_threshold # Load button coords from config - btn_path = Path(__file__).absolute().parent / 'config' / 'buttons.json' + btn_path = 'fgobot/config/buttons.json' with open(btn_path) as f: self.buttons = json.load(f) @@ -166,7 +168,7 @@ def __wait_until(self, im: str): while not self.__exists(im): self.__wait(INTERVAL_MID) - def __get_current_stage(self) -> int: + def get_current_stage(self) -> int: """ Get the current stage in battle. @@ -189,6 +191,7 @@ def __get_current_stage(self) -> int: def __find_friend(self) -> str: self.__wait_until('refresh_friends') + self.__wait(INTERVAL_SHORT) for _ in range(6): for fid in range(self.friend_count): im = 'f_{}'.format(fid) @@ -198,16 +201,19 @@ def __find_friend(self) -> str: self.__wait(INTERVAL_SHORT) return '' - def __enter_battle(self) -> bool: + def __enter_battle(self, first: bool = None) -> bool: """ Enter the battle. :return: whether successful. """ - self.__wait_until('menu') - while not self.__find_and_tap('quest', threshold=self.quest_threshold): - self.__swipe('quest') - self.__wait(INTERVAL_SHORT) + if first: + self.__wait_until('menu') + while not self.__find_and_tap('quest', threshold=self.quest_threshold): + self.__swipe('quest') + self.__wait(INTERVAL_SHORT) + else: + self.__end_battle(continue_battle=True) self.__wait(INTERVAL_MID) # no enough AP @@ -216,6 +222,10 @@ def __enter_battle(self) -> bool: return False else: ok = False + # self.__find_and_tap('scoll_bar') + # self.__wait(1) + self.__swipe('ap') + self.__wait(INTERVAL_SHORT) for ap_item in self.ap: if self.__find_and_tap(ap_item): self.__wait(1) @@ -232,12 +242,14 @@ def __enter_battle(self) -> bool: self.__find_and_tap('refresh_friends') self.__wait(INTERVAL_SHORT) self.__find_and_tap('yes') - self.__wait(INTERVAL_LONG) friend = self.__find_friend() + # TODO: reduce wait time to 2.0(4xINTERVAL_SHORT) + self.__wait(INTERVAL_LONG) self.__find_and_tap(friend) self.__wait(INTERVAL_SHORT) - self.__wait_until('start_quest') - self.__find_and_tap('start_quest') + if first: + self.__wait_until('start_quest') + self.__find_and_tap('start_quest') self.__wait(INTERVAL_SHORT) self.__wait_until('attack') return True @@ -250,7 +262,7 @@ def __play_battle(self) -> int: """ rounds = 0 while True: - stage = self.__get_current_stage() + stage = self.get_current_stage() if stage == -1: logger.error("Failed to get current stage. Leaving battle...") return rounds @@ -260,27 +272,39 @@ def __play_battle(self) -> int: .format(stage, self.stage_count, rounds)) self.stage_handlers[stage]() + flag = False + if stage == 3: + flag = True + while True: self.__wait(INTERVAL_MID) if self.__exists('bond') or self.__exists('bond_up'): logger.info("'与从者的羁绊' detected. Leaving battle...") return rounds elif self.__exists('attack'): - logger.info("'Attack' detected. Continuing loop...") - break - - def __end_battle(self): - # self.__find_and_tap('bond') - # self.__wait(INTERVAL_SHORT) - # self.__wait_until('gain_exp') - # self.__find_and_tap('gain_exp') - # self.__wait(INTERVAL_SHORT) + logger.debug("'Attack' detected. Continuing to next stage...") + if flag: + # TODO: brave card first + self.attack([1, 2, 3]) + self.__wait(INTERVAL_MID) + else: + break + + def __end_battle(self, continue_battle: bool = None): + """ + End the battle, or continue to the next battle + + :param continue_battle: 是否连续初级 + + :return: count of rounds. + """ self.__wait(INTERVAL_MID) while not self.__exists('next_step'): self.device.tap_rand(640, 360, 50, 50) self.__wait(INTERVAL_MID) self.__find_and_tap('next_step') + cv2.imwrite('screenshots/{}.jpg'.format(time.strftime("%Y%m%d-%H%M%S")), self.tm.screen) self.__wait(INTERVAL_MID) # quest first-complete reward @@ -292,7 +316,16 @@ def __end_battle(self): if self.__exists('not_apply'): self.__find_and_tap('not_apply') - self.__wait_until('menu') + self.__wait_until('continue_battle') + + if continue_battle: + # 连续战斗(点击连续出击,触发ap_regen或者从者的选择) + self.__find_and_tap('continue_battle') + else: + # TODO: exit for 1st exp card quest everyday + # 退出战斗(点击关闭,退出到菜单页面) + self.__find_and_tap('close') + self.__wait_until('menu') def at_stage(self, stage: int): """ @@ -331,9 +364,14 @@ def use_skill(self, servant: int, skill: int, obj=None): x, y, w, h = self.__button('choose_object') x += self.buttons['choose_object_distance'] * (obj - 1) self.device.tap_rand(x, y, w, h) + time.sleep(0.01) + self.device.tap_rand(x, y, w, h) logger.debug('Chose skill object {}.'.format(obj)) + else: + time.sleep(0.01) + self.device.tap_rand(x, y, w, h) - self.__wait(INTERVAL_SHORT * 2) + self.__wait(INTERVAL_SHORT) def use_master_skill(self, skill: int, obj=None, obj2=None): """ @@ -408,8 +446,10 @@ def attack(self, cards: list): x += self.buttons['card_distance'] * (card - 1) self.device.tap_rand(x, y, w, h) elif 6 <= card <= 8: + # self.__wait(INTERVAL_SHORT * 2) x, y, w, h = self.__button('noble_card') x += self.buttons['card_distance'] * (card - 6) + sleep(INTERVAL_MID * 2) self.device.tap_rand(x, y, w, h) else: logger.error('Card number must be in range [1, 8]') @@ -421,14 +461,18 @@ def run(self, max_loops: int = 10): :param max_loops: the max number of loops. """ count = 0 - for n_loop in range(max_loops): + self.__enter_battle(first=True) + self.__play_battle() + for n_loop in range(1, max_loops): logger.info('Entering battle...') - if not self.__enter_battle(): + if not self.__enter_battle(first=False): logger.info('AP runs out. Quiting...') break rounds = self.__play_battle() - self.__end_battle() count += 1 - logger.info('{}-th Battle complete. {} rounds played.'.format(count, rounds)) - - logger.info('{} Battles played in total. Good bye!'.format(count)) + if count == max_loops - 1: + logger.info('{} Battles played in total. Good bye!'.format(count + 1)) + break + else: + logger.info('{}-th Battle complete. {} rounds played.'.format(count + 1, rounds)) + self.__end_battle(continue_battle=False) diff --git a/fgobot/config/buttons.json b/fgobot/config/buttons.json index 56075b6..bceca52 100644 --- a/fgobot/config/buttons.json +++ b/fgobot/config/buttons.json @@ -61,6 +61,7 @@ "change_distance": 200, "swipe": { "friend": [600, 602, 600, 400], - "quest": [960, 588, 960, 400] + "quest": [960, 588, 960, 400], + "ap": [900, 500, 900, 400] } } \ No newline at end of file diff --git a/fgobot/images/ap_regen.png b/fgobot/images/ap_regen.png new file mode 100644 index 0000000..553d026 Binary files /dev/null and b/fgobot/images/ap_regen.png differ diff --git a/fgobot/images/bond_up.png b/fgobot/images/bond_up.png new file mode 100644 index 0000000..d2e7b1d Binary files /dev/null and b/fgobot/images/bond_up.png differ diff --git a/fgobot/images/bronze_apple.png b/fgobot/images/bronze_apple.png new file mode 100644 index 0000000..8409247 Binary files /dev/null and b/fgobot/images/bronze_apple.png differ diff --git a/fgobot/images/change.png b/fgobot/images/change.png new file mode 100644 index 0000000..92a0809 Binary files /dev/null and b/fgobot/images/change.png differ diff --git a/fgobot/images/change_disabled.png b/fgobot/images/change_disabled.png new file mode 100644 index 0000000..5a579bf Binary files /dev/null and b/fgobot/images/change_disabled.png differ diff --git a/fgobot/images/continue_battle.png b/fgobot/images/continue_battle.png new file mode 100644 index 0000000..762de3a Binary files /dev/null and b/fgobot/images/continue_battle.png differ diff --git a/fgobot/images/decide.png b/fgobot/images/decide.png new file mode 100644 index 0000000..134ef9d Binary files /dev/null and b/fgobot/images/decide.png differ diff --git a/fgobot/images/gold_apple.png b/fgobot/images/gold_apple.png new file mode 100644 index 0000000..064a413 Binary files /dev/null and b/fgobot/images/gold_apple.png differ diff --git a/fgobot/images/menu.png b/fgobot/images/menu.png index ae4bc4a..98836e3 100644 Binary files a/fgobot/images/menu.png and b/fgobot/images/menu.png differ diff --git a/fgobot/images/not_apply.png b/fgobot/images/not_apply.png index 3e77704..46a4252 100644 Binary files a/fgobot/images/not_apply.png and b/fgobot/images/not_apply.png differ diff --git a/fgobot/images/refresh_friends.png b/fgobot/images/refresh_friends.png index 23f7f4a..a13442b 100644 Binary files a/fgobot/images/refresh_friends.png and b/fgobot/images/refresh_friends.png differ diff --git a/fgobot/images/silver_apple.png b/fgobot/images/silver_apple.png new file mode 100644 index 0000000..a448c54 Binary files /dev/null and b/fgobot/images/silver_apple.png differ diff --git a/fgobot/images/start_quest.png b/fgobot/images/start_quest.png index a17db06..79ab36e 100644 Binary files a/fgobot/images/start_quest.png and b/fgobot/images/start_quest.png differ diff --git a/free.png b/free.png new file mode 100644 index 0000000..68b3b51 Binary files /dev/null and b/free.png differ diff --git a/friend_merlin.png b/friend_merlin.png new file mode 100644 index 0000000..1b69c61 Binary files /dev/null and b/friend_merlin.png differ diff --git a/friend_merlin1.png b/friend_merlin1.png new file mode 100644 index 0000000..a3a13fa Binary files /dev/null and b/friend_merlin1.png differ diff --git a/friend_qp.png b/friend_qp.png index 1ff3b0e..38d4daf 100644 Binary files a/friend_qp.png and b/friend_qp.png differ diff --git a/friend_sikaha.png b/friend_sikaha.png new file mode 100644 index 0000000..68b4dd3 Binary files /dev/null and b/friend_sikaha.png differ diff --git a/friend_sikaha2.png b/friend_sikaha2.png new file mode 100644 index 0000000..6c341ba Binary files /dev/null and b/friend_sikaha2.png differ diff --git a/my_bot.py b/my_bot.py index 6741402..a15b03c 100644 --- a/my_bot.py +++ b/my_bot.py @@ -21,7 +21,7 @@ # AP策略为:当体力耗尽时,优先吃银苹果,再吃金苹果 # 如果不指定ap参数,则当体力耗尽时停止运行 - ap=['silver_apple', 'gold_apple'], + # ap=['silver_apple', 'gold_apple'], # 要打的关卡有3面 stage_count=3, @@ -45,27 +45,23 @@ @bot.at_stage(1) def stage_1(): # s(1, 1)表示使用1号从者的技能1 - s(1, 1) - s(2, 1) - s(3, 1) - s(3, 3) + s(2, 3) # 大英雄充能 # (a[6, 1, 2])表示出卡顺序为:6号卡(1号从者宝具卡),1号卡,2号卡 - a([6, 1, 2]) + a([7, 1, 2]) # 大英雄宝具卡 # 第二面的打法 @bot.at_stage(2) def stage_2(): # m(2, 1)表示使用御主技能2,对象为1号从者 - m(2, 1) - a([6, 1, 2]) + m(3, 3) # 拉尔群冲 + a([7, 1, 2]) # 大流士宝具卡 # 第三面的打法 @bot.at_stage(3) def stage_3(): - s(1, 3) - a([6, 1, 2]) + a([6, 1, 2]) # 狂兰宝具卡 # 程序的入口点(不加这行也可以) @@ -74,7 +70,7 @@ def stage_3(): # 检查设备是否连接 if not bot.device.connected(): # 如果没有连接,则尝试通过本地端口62001连接(具体参数请参考自己的设备/模拟器) - bot.device.connect('127.0.0.1:62001') + bot.device.connect('127.0.0.1:7555') # 启动bot,最多打5次 - bot.run(max_loops=5) + # bot.run(max_loops=5) diff --git a/qp.png b/qp.png index 7da3a01..2f728a6 100644 Binary files a/qp.png and b/qp.png differ diff --git a/qp.py b/qp.py new file mode 100644 index 0000000..9cf7b62 --- /dev/null +++ b/qp.py @@ -0,0 +1,79 @@ +import logging +import datetime +import argparse + +from fgobot import BattleBot + + +# 指定日志的输出等级(DEBUG / INFO / WARNING / ERROR) +logging.basicConfig(level=logging.INFO) + +# 实例化一个bot +bot = BattleBot( + + # 要打的关卡截图为'qp.png',放在这个文件的同一级目录下 + quest='qp.png', + + # 需要的助战截图为'friend_qp.png',放在这个文件的同一级目录下 + # 如果可以接受的助战有多个,可以传入一个list,例如:friend=['friend1.png', 'friend2.png] + friend=['friend_qp.png'], + + # AP策略为:当体力耗尽时,优先吃银苹果,再吃金苹果 + # 如果不指定ap参数,则当体力耗尽时停止运行 + ap=['gold_apple'], + + # 要打的关卡有3面 + stage_count=3, + + # 关卡图像识别的阈值为0.97 + # 如果设的过低会导致进错本,太高会导致无法进本,请根据实际情况调整 + quest_threshold=0.97, + + # 助战图像识别的阈值为0.95 + # 如果设的过低会导致选择错误的助战,太高会导致选不到助战,请根据实际情况调整 + friend_threshold=0.85 +) + +# 为了方便,使用了简写 +s = bot.use_skill +m = bot.use_master_skill +a = bot.attack + + +# 第一面的打法 +@bot.at_stage(1) +def stage_1(): + s(3, 1) # 梅林群冲 + a([7, 1, 2]) # 尼托宝具卡 + + +# 第二面的打法 +@bot.at_stage(2) +def stage_2(): + # m(2, 1)表示使用御主技能2,对象为1号从者 + s(2, 2) # 尼托自冲 + a([7, 1, 2]) # 尼托宝具卡 + + +# 第三面的打法 +@bot.at_stage(3) +def stage_3(): + a([6, 1, 2]) # 宝具卡 + + +# 程序的入口点(不加这行也可以) +# 使用时,可以直接在命令行运行'python my_bot.py' +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-p', dest='port', default=62001, help='adb device port') + parser.add_argument('-n', dest='number_loops', default=3, help='number of loops to be ran') + parser.add_argument('-v', dest='verbosity', action='store_true', help='debug or not') + args = parser.parse_args() + + # 检查设备是否连接 + if not bot.device.connected(): + # 如果没有连接,则尝试通过本地端口62001连接(具体参数请参考自己的设备/模拟器) + bot.device.connect(f'127.0.0.1:{args.port}') + + # 启动bot,最多打5次 + bot.run(max_loops=int(args.number_loops)) diff --git a/santa2020.py b/santa2020.py new file mode 100644 index 0000000..b54776f --- /dev/null +++ b/santa2020.py @@ -0,0 +1,89 @@ +""" +An example of custom battle bot. +:author: @will7101 +""" + +from fgobot import BattleBot +import logging + +# 指定日志的输出等级(DEBUG / INFO / WARNING / ERROR) +logging.basicConfig(level=logging.INFO) + +# 实例化一个bot +bot = BattleBot( + + # 要打的关卡截图为'qp.png',放在这个文件的同一级目录下 + quest='final.png', + + # 需要的助战截图为'friend_qp.png',放在这个文件的同一级目录下 + # 如果可以接受的助战有多个,可以传入一个list,例如:friend=['friend1.png', 'friend2.png] + friend='friend_sikaha.png', + + # AP策略为:当体力耗尽时,优先吃银苹果,再吃金苹果 + # 如果不指定ap参数,则当体力耗尽时停止运行 + ap=['gold_apple'], + + # 要打的关卡有3面 + stage_count=3, + + # 关卡图像识别的阈值为0.97 + # 如果设的过低会导致进错本,太高会导致无法进本,请根据实际情况调整 + quest_threshold=0.97, + + # 助战图像识别的阈值为0.95 + # 如果设的过低会导致选择错误的助战,太高会导致选不到助战,请根据实际情况调整 + friend_threshold=0.95 +) + +# 为了方便,使用了简写 +s = bot.use_skill +m = bot.use_master_skill +a = bot.attack + +""" +1. 斯卡哈 +2. 女武神 +3. 斯卡哈 +""" + + +# 第一面的打法 +@bot.at_stage(1) +def stage_1(): + # s(1, 1)表示使用1号从者的技能1 + s(1, 1, obj=2) # 斯卡哈绿魔放 + s(3, 1, obj=2) # 斯卡哈绿魔放 + s(2, 1) # 女武神绿魔放 + s(2, 3) # 女武神缓充能 + # m(2, 1)表示使用御主技能2,对象为1号从者 + m(2, 2) # 冲能服 + # (a[6, 1, 2])表示出卡顺序为:6号卡(1号从者宝具卡),1号卡,2号卡 + a([7, 1, 2]) # 女武神宝具卡 + + +# 第二面的打法 +@bot.at_stage(2) +def stage_2(): + s(1, 3, obj=2) # 斯卡哈充能 + a([7, 1, 2]) # 女武神宝具卡 + + +# 第三面的打法 +@bot.at_stage(3) +def stage_3(): + s(1, 2) # 斯卡哈减防 + s(3, 2) # 斯卡哈减防 + s(3, 3, obj=2) # 斯卡哈充能 + a([7, 1, 2]) # 女武神宝具卡 + + +# 程序的入口点(不加这行也可以) +# 使用时,可以直接在命令行运行'python my_bot.py' +if __name__ == '__main__': + # 检查设备是否连接 + if not bot.device.connected(): + # 如果没有连接,则尝试通过本地端口62001连接(具体参数请参考自己的设备/模拟器) + bot.device.connect('127.0.0.1:62001') + + # 启动bot,最多打5次 + bot.run(max_loops=5000)