Skip to content
This repository has been archived by the owner on Mar 1, 2020. It is now read-only.

20191223WGupdated #3

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ m4/lt~obsolete.m4
autom4te.cache

/.idea
xp_bot_ac.py
qp_bot.py
fgobot/device-wg.py
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ fgo自动脚本

在命令行中输入`adb`,出现帮助信息则说明安装成功。

如果你的python在wsl下运行,但模拟器在windows下运行,请使用windows下的adb(而不是wsl下的),否则很容易莫名断开
即在device.py中将`self.adb_path = adb_path`改为`self.adb_path = /mnt/c/adb/adb.exe`的形式

### 安装本体

下载这个项目,在根目录下运行
Expand All @@ -25,21 +28,19 @@ python3 setup.py install
```

## 使用前的准备
- 推荐使用模拟器进行游戏,并调整分辨率为`1280x720`。**使用真机可能导致分辨率和连接等问题。**
- 推荐使用模拟器进行游戏,并调整分辨率为`1280x720`。**使用真机可能导致分辨率和连接等问题,使用其他分辨率可能导致图片识别缓慢的性能问题。**
- 如果使用`1280x720`分辨率的等比缩放分辨率(例如`1920x1080`),可以修改my_bot.py中的相应参数,但除了`1280x720`和`1920x1080`分辨率以外,需要自行制作截图
- 构筑所用的队伍。由于当前版本不支持获取敌方信息,建议选择稳定的宝具速刷队。
- 在设置中关闭“灵基再临第四阶段展示”选项,可以减少助战识别的工作量
- 在助战界面选择好想要的职介,确保(在线的)好友可以大概率刷出期望的助战。
- 在设置中关闭“灵基再临第四阶段展示”选项,可以减少助战识别的工作量
- 在助战界面选择好想要的职阶,确保(在线的)好友可以大概率刷出期望的助战。
- 手动进行一场对战。(非必须,用于选中队伍以及调整相关设置)
- 在战斗菜单中关闭“技能使用确认”和“宝具匀速”,打开“缩短敌人消失时间。
- 在选择指令卡界面调整游戏速度为2倍速。
- 在设备/模拟器上对要打的副本和期望的助战截图,放在脚本的同级目录下,可以参考`/qp.png`和`/friend_qp.png`。
![quest](https://github.com/will7101/fgo-bot/blob/master/qp.png?raw=true)

![friend](https://github.com/will7101/fgo-bot/blob/master/friend_qp.png?raw=true)
- 在设备/模拟器上对要打的副本和期望的助战截图,放在脚本的`userimages`目录下,可以参考`userimages/qp.png`和`userimages/friend_qp.png`。

注意:尽量截取更多的信息,但不要超出可点击的范围。

- 将游戏置于进入任务前的界面。
- 将游戏置于进入任务前的界面,或者战斗中的界面,但开始运行程序时,战斗必须处于停顿、等待玩家输入的状态(即可以看到“攻击”按钮的状态)

## 使用教程

Expand Down Expand Up @@ -67,6 +68,17 @@ python3 setup.py install

选取指令卡并攻击。

`cards`需要为有三个整数作为元素的`list`,按照顺序表示出的卡。其中`1~5`表示从左往右的常规卡,`6~8`表示从左往右的宝具卡。
`cards`需要为有零到四个整数作为元素的`list`,按照顺序表示出的卡。其中`1~5`表示从左往右的常规卡,`6~8`表示从左往右的宝具卡。

例如`[6, 1, 2]`表示先使用从者1的宝具卡,再使用指令卡1和指令卡2。

## 更新内容(byWG)
- 更新到国服最新截图(2019年12月)
- 支持多种分辨率
- 支持从任务选择界面、战斗界面、战斗结算界面开始运行程序
- 战斗脚本支持根据不同回合采用不同的出卡(因而支持放完宝具之后自动平砍)
- 支持随机出卡,且防止因AP不足放不出宝具而卡住,每次都会选择4张卡
- 缩短等待时间
- 如脚本中的技能正在冷却而无法释放,也能继续运行下去
- 其他微小调整

例如`[6, 1, 2]`表示先使用从者1的宝具卡,再使用指令卡1和指令卡2。
149 changes: 101 additions & 48 deletions fgobot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import logging
import random
from functools import partial
from typing import Dict, Any, Callable
from .tm import TM
Expand All @@ -11,13 +12,12 @@
from pathlib import Path
from time import sleep
from typing import Tuple, List, Union
from random import randint

logger = logging.getLogger('bot')

INTERVAL_SHORT = 1
INTERVAL_MID = 2
INTERVAL_LONG = 10
INTERVAL_SHORT = 0.5
INTERVAL_MID = 1.0
INTERVAL_LONG = 10.0


class BattleBot:
Expand All @@ -28,10 +28,11 @@ class BattleBot:
def __init__(self,
quest: str = 'quest.png',
friend: Union[str, List[str]] = 'friend.png',
stage_count = 3,
stage_count: int = 3,
ap: List[str] = None,
quest_threshold: float = 0.97,
friend_threshold: float = 0.97
friend_threshold: float = 0.97,
zoom_width: int = 720
):
"""

Expand Down Expand Up @@ -60,8 +61,11 @@ def __init__(self,
# Device
self.device = Device()

self.zoom_ratio = float(zoom_width/720)

# Template matcher
self.tm = TM(feed=partial(self.device.capture, method=Device.FROM_SHELL))
self.tm = TM(feed=partial(
self.device.capture, method=Device.FROM_SHELL), zoom_ratio=self.zoom_ratio)

# Target quest
path = Path(quest).absolute()
Expand Down Expand Up @@ -89,6 +93,7 @@ def __init__(self,
btn_path = Path(__file__).absolute().parent / 'config' / 'buttons.json'
with open(btn_path) as f:
self.buttons = json.load(f)
self.__zoom(self.buttons)

logger.debug('Bot initialized.')

Expand All @@ -99,10 +104,20 @@ def __add_stage_handler(self, stage: int, f: Callable):
:param stage: the stage number
:param f: the handler function
"""
assert not self.stage_handlers.get(stage), 'Cannot register multiple function to a single stage.'
logger.debug('Function {} registered to stage {}'.format(f.__name__, stage))
assert not self.stage_handlers.get(
stage), 'Cannot register multiple function to a single stage.'
logger.debug(
'Function {} registered to stage {}'.format(f.__name__, stage))
self.stage_handlers[stage] = f

def __zoom(self, b):
for key, value in b.items():
if isinstance(value, dict):
for k, v in value.items():
b[key][k] = int(v*self.zoom_ratio)
else:
b[key] = int(value*self.zoom_ratio)

def __button(self, btn):
"""
Return the __button coords and size.
Expand All @@ -120,8 +135,7 @@ def __swipe(self, track):
:param track:
:return:
"""
x1, y1, x2, y2 = map(lambda x: x + randint(-5, 5), self.buttons['swipe'][track])
self.device.swipe((x1, y1), (x2, y2))
self.device.swipe_rand(**self.buttons[track])

def __find_and_tap(self, im: str, threshold: float = None) -> bool:
"""
Expand All @@ -147,7 +161,7 @@ def __exists(self, im: str, threshold: float = None) -> bool:
"""
return self.tm.exists(im, threshold=threshold)

def __wait(self, sec):
def __wait(self, sec: float):
"""
Wait some seconds and update the screen feed.

Expand All @@ -164,7 +178,7 @@ def __wait_until(self, im: str):
logger.debug("Wait until image '{}' appears.".format(im))
self.tm.update_screen()
while not self.__exists(im):
self.__wait(INTERVAL_MID)
self.__wait(INTERVAL_SHORT)

def __get_current_stage(self) -> int:
"""
Expand All @@ -189,12 +203,12 @@ def __get_current_stage(self) -> int:

def __find_friend(self) -> str:
self.__wait_until('refresh_friends')
for _ in range(6):
for _ in range(9):
for fid in range(self.friend_count):
im = 'f_{}'.format(fid)
if self.__exists(im, threshold=self.friend_threshold):
return im
self.__swipe('friend')
self.__swipe('swipe_friend')
self.__wait(INTERVAL_SHORT)
return ''

Expand All @@ -206,7 +220,7 @@ def __enter_battle(self) -> bool:
"""
self.__wait_until('menu')
while not self.__find_and_tap('quest', threshold=self.quest_threshold):
self.__swipe('quest')
self.__swipe('swipe_quest')
self.__wait(INTERVAL_SHORT)
self.__wait(INTERVAL_MID)

Expand Down Expand Up @@ -248,23 +262,30 @@ def __play_battle(self) -> int:

:return: count of rounds.
"""
all_rounds = 0
rounds = 0
last_stage = 0
while True:
stage = self.__get_current_stage()
if stage == -1:
logger.error("Failed to get current stage. Leaving battle...")
return rounds

rounds += 1
logger.info('At stage {}/{}, round {}, calling handler function...'
return all_rounds
if stage == last_stage:
rounds += 1
else:
last_stage = stage
all_rounds = all_rounds+rounds
rounds = 1
logger.info('At stage {}/{}, {} rounds for this stage,calling handler function...'
.format(stage, self.stage_count, rounds))
self.stage_handlers[stage]()
self.stage_handlers[stage](rounds)

while True:
self.__wait(INTERVAL_MID)
if self.__exists('bond') or self.__exists('bond_up'):
logger.info("'与从者的羁绊' detected. Leaving battle...")
return rounds
if self.__exists('bond'):
all_rounds = all_rounds+rounds
logger.info("'Bond' detected. Leaving battle...")
return all_rounds
elif self.__exists('attack'):
logger.info("'Attack' detected. Continuing loop...")
break
Expand All @@ -283,16 +304,20 @@ def __end_battle(self):
self.__find_and_tap('next_step')
self.__wait(INTERVAL_MID)

# quest first-complete reward
if self.__exists('please_tap'):
self.__find_and_tap('please_tap')
self.__wait(INTERVAL_SHORT)
while not self.__exists('menu'):
# not send friend application
if self.__exists('not_apply'):
self.__find_and_tap('not_apply')
self.__wait(INTERVAL_SHORT)

# not send friend application
if self.__exists('not_apply'):
self.__find_and_tap('not_apply')
# quest first-complete reward
if self.__exists('please_tap'):
self.__find_and_tap('please_tap')
self.__wait(INTERVAL_SHORT)

self.__wait_until('menu')
self.__wait(INTERVAL_SHORT)

# self.__wait_until('menu')

def at_stage(self, stage: int):
"""
Expand Down Expand Up @@ -332,8 +357,14 @@ def use_skill(self, servant: int, skill: int, obj=None):
x += self.buttons['choose_object_distance'] * (obj - 1)
self.device.tap_rand(x, y, w, h)
logger.debug('Chose skill object {}.'.format(obj))
elif self.__exists('skill'):
self.__wait(INTERVAL_SHORT)
if self.__exists('skill'):
logger.error(
'Skill ({}, {}) not ready.'.format(servant, skill))
self.device.tap_rand(x, y, w, h)

self.__wait(INTERVAL_SHORT * 2)
#self.__wait(INTERVAL_SHORT * 2)

def use_master_skill(self, skill: int, obj=None, obj2=None):
"""
Expand Down Expand Up @@ -377,13 +408,20 @@ def use_master_skill(self, skill: int, obj=None, obj2=None):

x += self.buttons['change_distance'] * (obj2 - obj)
self.device.tap_rand(x, y, w, h)
logger.debug('Chose master skill object ({}, {}).'.format(obj, obj2))
logger.debug(
'Chose master skill object ({}, {}).'.format(obj, obj2))

self.__wait(INTERVAL_SHORT)
self.__find_and_tap('change')
logger.debug('Order Change')
else:
logger.error('Invalid master skill object.')
elif self.__exists('skill'):
self.__wait(INTERVAL_SHORT)
if self.__exists('skill'):
logger.error('Master skill {} not ready.'.format(skill))
x, y, w, h = self.__button('master_skill_menu')
self.device.tap_rand(x, y, w, h)

self.__wait(INTERVAL_SHORT)

Expand All @@ -396,12 +434,17 @@ def attack(self, cards: list):
:param cards: the cards id, as a list

"""
assert len(cards) == 3, 'Number of cards must be 3.'
assert len(set(cards)) == 3, 'Cards must be distinct.'
while len(cards) < 4:
x = random.randrange(1, 6)
if x in cards:
continue
cards.append(x)
assert len(cards) == 4, 'Number of cards must be 4.'
assert len(set(cards)) == 4, 'Cards must be distinct.'
self.__wait_until('attack')

self.__find_and_tap('attack')
self.__wait(INTERVAL_SHORT * 2)
self.__wait(INTERVAL_MID)
for card in cards:
if 1 <= card <= 5:
x, y, w, h = self.__button('card')
Expand All @@ -421,14 +464,24 @@ def run(self, max_loops: int = 10):
:param max_loops: the max number of loops.
"""
count = 0
for n_loop in range(max_loops):
logger.info('Entering battle...')
if not self.__enter_battle():
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))
rounds = 0
for n_loop in range(1, max_loops+1):
self.__wait(INTERVAL_MID)
if self.__exists('menu'):
logger.info('Entering battle...')
if not self.__enter_battle():
logger.info('AP runs out. Quiting...')
break
rounds = self.__play_battle()
self.__end_battle()
elif self.__exists('attack'):
logger.info('Starting battle halfway...')
rounds = self.__play_battle()
self.__end_battle()
elif self.__exists('next_step'):
logger.info('Looks like a finished battle...')
self.__end_battle()
logger.info(
'{}-th Battle complete. {} rounds played.'.format(n_loop, rounds))

logger.info('{} Battles played in total. Good bye!'.format(n_loop))
Loading