├── __init__.py ├── targets ├── Book.jpg ├── Box.jpg ├── Chair.jpg ├── Cloth.jpg ├── Coal.jpg ├── Food.jpg ├── Heels.jpg ├── Kit.jpg ├── Lamp.jpg ├── Mic.jpg ├── Mine.jpg ├── Oil.jpg ├── Quilt.jpg ├── Screw.jpg ├── Silk.jpg ├── Sofa.jpg ├── Steel.jpg ├── Straw.jpg ├── Wood.jpg ├── Bottle.jpg ├── Chicken.jpg ├── Cotton.jpg ├── FlatCar.jpg ├── Helmet.jpg ├── Potting.jpg ├── Chlorine.jpg ├── Computer.jpg ├── Cupboard.jpg ├── Fertilizer.jpg ├── Gasoline.jpg ├── Rank_btn.jpg ├── Schoolbag.jpg ├── Vegetable.jpg ├── Refrigerator.jpg └── BeachUmbrella.jpg ├── config.json ├── main.py ├── flusher.py ├── LICENSE ├── target.py ├── config.py ├── .gitignore ├── cv.py ├── prop.py ├── README.md └── automator.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /targets/Book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Book.jpg -------------------------------------------------------------------------------- /targets/Box.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Box.jpg -------------------------------------------------------------------------------- /targets/Chair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Chair.jpg -------------------------------------------------------------------------------- /targets/Cloth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Cloth.jpg -------------------------------------------------------------------------------- /targets/Coal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Coal.jpg -------------------------------------------------------------------------------- /targets/Food.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Food.jpg -------------------------------------------------------------------------------- /targets/Heels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Heels.jpg -------------------------------------------------------------------------------- /targets/Kit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Kit.jpg -------------------------------------------------------------------------------- /targets/Lamp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Lamp.jpg -------------------------------------------------------------------------------- /targets/Mic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Mic.jpg -------------------------------------------------------------------------------- /targets/Mine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Mine.jpg -------------------------------------------------------------------------------- /targets/Oil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Oil.jpg -------------------------------------------------------------------------------- /targets/Quilt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Quilt.jpg -------------------------------------------------------------------------------- /targets/Screw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Screw.jpg -------------------------------------------------------------------------------- /targets/Silk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Silk.jpg -------------------------------------------------------------------------------- /targets/Sofa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Sofa.jpg -------------------------------------------------------------------------------- /targets/Steel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Steel.jpg -------------------------------------------------------------------------------- /targets/Straw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Straw.jpg -------------------------------------------------------------------------------- /targets/Wood.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Wood.jpg -------------------------------------------------------------------------------- /targets/Bottle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Bottle.jpg -------------------------------------------------------------------------------- /targets/Chicken.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Chicken.jpg -------------------------------------------------------------------------------- /targets/Cotton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Cotton.jpg -------------------------------------------------------------------------------- /targets/FlatCar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/FlatCar.jpg -------------------------------------------------------------------------------- /targets/Helmet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Helmet.jpg -------------------------------------------------------------------------------- /targets/Potting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Potting.jpg -------------------------------------------------------------------------------- /targets/Chlorine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Chlorine.jpg -------------------------------------------------------------------------------- /targets/Computer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Computer.jpg -------------------------------------------------------------------------------- /targets/Cupboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Cupboard.jpg -------------------------------------------------------------------------------- /targets/Fertilizer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Fertilizer.jpg -------------------------------------------------------------------------------- /targets/Gasoline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Gasoline.jpg -------------------------------------------------------------------------------- /targets/Rank_btn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Rank_btn.jpg -------------------------------------------------------------------------------- /targets/Schoolbag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Schoolbag.jpg -------------------------------------------------------------------------------- /targets/Vegetable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Vegetable.jpg -------------------------------------------------------------------------------- /targets/Refrigerator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/Refrigerator.jpg -------------------------------------------------------------------------------- /targets/BeachUmbrella.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwayneten/JGM-Automator/HEAD/targets/BeachUmbrella.jpg -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "swipe_interval_sec": 5, 3 | "upgrade_interval_sec": 10, 4 | "building_pos": [ 5 | ["零件厂", "人民石油", "企鹅机械"], 6 | ["商贸中心", "媒体之声", "民食斋"], 7 | ["花园洋房", "复兴公馆", "小型公寓"] 8 | ], 9 | "train_get_rank": [2], 10 | "debug_mode": false, 11 | "upgrade_building": true, 12 | "upgrade_building_list": [7], 13 | "refresh_train": true, 14 | "detect_goods": true 15 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from automator import Automator 2 | from prop import END, RUN 3 | 4 | from flusher import flush 5 | from multiprocessing import Process, Queue 6 | 7 | KEYBOARD = Queue() 8 | 9 | 10 | def main(kb): 11 | # 连接 adb 。 12 | instance = Automator('127.0.0.1:7555', kb) 13 | 14 | # 启动脚本。 15 | instance.start() 16 | 17 | 18 | if __name__ == '__main__': 19 | p = Process(target=main, args=(KEYBOARD,)) 20 | p.start() 21 | while True: 22 | flush() 23 | txt = input() 24 | if txt == END or txt.split(' ')[0] == RUN: 25 | KEYBOARD.put(txt) 26 | if txt == END: 27 | break 28 | else: 29 | KEYBOARD.put('') 30 | p.join() 31 | -------------------------------------------------------------------------------- /flusher.py: -------------------------------------------------------------------------------- 1 | class _Flush: 2 | def __init__(self): 3 | try: 4 | self.work = _FlushUnix() 5 | except ModuleNotFoundError: 6 | self.work = _FlushWindows() 7 | 8 | def __call__(self): return self.work() 9 | 10 | class _FlushUnix: 11 | def __init__(self): 12 | from termios import tcflush, TCIFLUSH 13 | 14 | def __call__(self): 15 | from termios import tcflush, TCIFLUSH 16 | import sys 17 | tcflush(sys.stdin, TCIFLUSH) 18 | 19 | class _FlushWindows: 20 | def __init__(self): 21 | import msvcrt 22 | 23 | def __call__(self): 24 | import msvcrt 25 | while msvcrt.kbhit(): 26 | msvcrt.getch() 27 | 28 | flush = _Flush() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 DwayneTen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /target.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TargetType(Enum): 5 | """ 6 | 货物枚举类型。通过截屏制作货物图片时,请确保截屏符合实际大小。 7 | """ 8 | Chair = 'targets/Chair.jpg' 9 | Vegetable = 'targets/Vegetable.jpg' 10 | Bottle = 'targets/Bottle.jpg' 11 | Wood = 'targets/Wood.jpg' 12 | Food = 'targets/Food.jpg' 13 | Box = 'targets/Box.jpg' 14 | Sofa = 'targets/Sofa.jpg' 15 | Coal = 'targets/Coal.jpg' 16 | Screw = 'targets/Screw.jpg' 17 | Book = 'targets/Book.jpg' 18 | Cloth = 'targets/Cloth.jpg' 19 | Cotton = 'targets/Cotton.jpg' 20 | Quilt = 'targets/Quilt.jpg' 21 | Straw = 'targets/Straw.jpg' 22 | Mine = 'targets/Mine.jpg' 23 | Chicken = 'targets/Chicken.jpg' 24 | Kit = 'targets/Kit.jpg' 25 | Lamp = 'targets/Lamp.jpg' 26 | Mic = 'targets/Mic.jpg' 27 | Oil = 'targets/Oil.jpg' 28 | Silk = 'targets/Silk.jpg' 29 | Fertilizer = 'targets/Fertilizer.jpg' 30 | Cupboard = 'targets/Cupboard.jpg' 31 | Heels = 'targets/Heels.jpg' 32 | Steel = 'targets/Steel.jpg' 33 | Computer = 'targets/Computer.jpg' 34 | Gasoline = 'targets/Gasoline.jpg' 35 | Potting = 'targets/Potting.jpg' 36 | Chlorine = 'targets/Chlorine.jpg' 37 | Schoolbag = 'targets/Schoolbag.jpg' 38 | Rank_btn = 'targets/Rank_btn.jpg' 39 | Refrigerator = 'targets/Refrigerator.jpg' 40 | FlatCar = 'targets/FlatCar.jpg' 41 | BeachUmbrella = 'targets/BeachUmbrella.jpg' 42 | Helmet = 'targets/Helmet.jpg' -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import prop 3 | from numpy import array 4 | from building import BuildingType 5 | 6 | CONFIG_FILE = './config.json' 7 | 8 | class Reader: 9 | building_pos = None 10 | goods_2_building_seq = None 11 | swipe_interval_sec = None 12 | upgrade_interval_sec = None 13 | debug_mode = False 14 | refresh_train = False 15 | detect_goods = False 16 | upgrade_building = False 17 | upgrade_building_list = None 18 | 19 | @staticmethod 20 | def _building_name_2_building_enum(building_name): 21 | for building in BuildingType: 22 | enum_name = str(building).split('.')[1] 23 | if enum_name == building_name: 24 | return building 25 | raise Exception(f'Wrong building name [{building_name}]') 26 | 27 | def _flatten_list(self, leveled_building_pos): 28 | tmp_list = array(leveled_building_pos[::-1]).flatten().tolist() 29 | return [self._building_name_2_building_enum(ele) for ele in tmp_list] 30 | 31 | @staticmethod 32 | def _generate_building_pos(flattened_building_pos): 33 | return [ele for ele in flattened_building_pos] 34 | 35 | @staticmethod 36 | def _generate_goods_2_building_seq(building_pos, train_get_rank): 37 | mask_building_pos = list(filter(lambda building: prop.BUILDING_RANK[building] in train_get_rank, building_pos)) 38 | 39 | res = {} 40 | for i, building in enumerate(building_pos): 41 | building_seq = i + 1 42 | goods = prop.BUILDING_2_GOODS[building] 43 | if goods is not None and building in mask_building_pos: 44 | res[goods] = building_seq 45 | return res 46 | 47 | def refresh(self): 48 | with open(CONFIG_FILE, 'r', encoding='utf-8') as f: 49 | config = json.load(f) 50 | self.swipe_interval_sec = config['swipe_interval_sec'] 51 | self.upgrade_interval_sec = config['upgrade_interval_sec'] 52 | flattened_building_pos = self._flatten_list(config['building_pos']) 53 | self.building_pos = self._generate_building_pos(flattened_building_pos) 54 | self.goods_2_building_seq = self._generate_goods_2_building_seq(self.building_pos, config['train_get_rank']) 55 | 56 | self.debug_mode = config['debug_mode'] 57 | self.detect_goods = config['detect_goods'] 58 | if self.detect_goods: 59 | self.refresh_train = config['refresh_train'] 60 | self.expect_target_rank = [0, 1, 2] 61 | for i in config['train_get_rank']: 62 | self.expect_target_rank.remove(i) 63 | self.goods_2_building_seq_excpet_target = self._generate_goods_2_building_seq(self.building_pos, self.expect_target_rank) 64 | self.upgrade_building = config['upgrade_building'] 65 | if self.upgrade_building: 66 | self.upgrade_building_list = config['upgrade_building_list'] 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # JetBrains 2 | .idea 3 | venv/ 4 | 5 | # Visual Studio Code 6 | .vscode 7 | 8 | 9 | # Created by https://www.gitignore.io/api/macos,python,opencv,windows 10 | # Edit at https://www.gitignore.io/?templates=macos,python,opencv,windows 11 | 12 | ### macOS ### 13 | # General 14 | .DS_Store 15 | .AppleDouble 16 | .LSOverride 17 | 18 | # Icon must end with two \r 19 | Icon 20 | 21 | # Thumbnails 22 | ._* 23 | 24 | # Files that might appear in the root of a volume 25 | .DocumentRevisions-V100 26 | .fseventsd 27 | .Spotlight-V100 28 | .TemporaryItems 29 | .Trashes 30 | .VolumeIcon.icns 31 | .com.apple.timemachine.donotpresent 32 | 33 | # Directories potentially created on remote AFP share 34 | .AppleDB 35 | .AppleDesktop 36 | Network Trash Folder 37 | Temporary Items 38 | .apdisk 39 | 40 | ### OpenCV ### 41 | #OpenCV for Mac and Linux 42 | #build and release folders 43 | */CMakeFiles 44 | */CMakeCache.txt 45 | */Makefile 46 | */cmake_install.cmake 47 | 48 | ### Python ### 49 | # Byte-compiled / optimized / DLL files 50 | __pycache__/ 51 | *.py[cod] 52 | *$py.class 53 | 54 | # C extensions 55 | *.so 56 | 57 | # Distribution / packaging 58 | .Python 59 | build/ 60 | develop-eggs/ 61 | dist/ 62 | downloads/ 63 | eggs/ 64 | .eggs/ 65 | lib/ 66 | lib64/ 67 | parts/ 68 | sdist/ 69 | var/ 70 | wheels/ 71 | pip-wheel-metadata/ 72 | share/python-wheels/ 73 | *.egg-info/ 74 | .installed.cfg 75 | *.egg 76 | MANIFEST 77 | 78 | # PyInstaller 79 | # Usually these files are written by a python script from a template 80 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 81 | *.manifest 82 | *.spec 83 | 84 | # Installer logs 85 | pip-log.txt 86 | pip-delete-this-directory.txt 87 | 88 | # Unit test / coverage reports 89 | htmlcov/ 90 | .tox/ 91 | .nox/ 92 | .coverage 93 | .coverage.* 94 | .cache 95 | nosetests.xml 96 | coverage.xml 97 | *.cover 98 | .hypothesis/ 99 | .pytest_cache/ 100 | 101 | # Translations 102 | *.mo 103 | *.pot 104 | 105 | # Scrapy stuff: 106 | .scrapy 107 | 108 | # Sphinx documentation 109 | docs/_build/ 110 | 111 | # PyBuilder 112 | target/ 113 | 114 | # pyenv 115 | .python-version 116 | 117 | # pipenv 118 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 119 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 120 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 121 | # install all needed dependencies. 122 | #Pipfile.lock 123 | 124 | # celery beat schedule file 125 | celerybeat-schedule 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | .spyproject 133 | 134 | # Rope project settings 135 | .ropeproject 136 | 137 | # Mr Developer 138 | .mr.developer.cfg 139 | .project 140 | .pydevproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | ### Windows ### 154 | # Windows thumbnail cache files 155 | Thumbs.db 156 | Thumbs.db:encryptable 157 | ehthumbs.db 158 | ehthumbs_vista.db 159 | 160 | # Dump file 161 | *.stackdump 162 | 163 | # Folder config file 164 | [Dd]esktop.ini 165 | 166 | # Recycle Bin used on file shares 167 | $RECYCLE.BIN/ 168 | 169 | # Windows Installer files 170 | *.cab 171 | *.msi 172 | *.msix 173 | *.msm 174 | *.msp 175 | 176 | # Windows shortcuts 177 | *.lnk 178 | 179 | # End of https://www.gitignore.io/api/macos,python,opencv,windows 180 | 181 | *.log 182 | tmp/* 183 | assets/* -------------------------------------------------------------------------------- /cv.py: -------------------------------------------------------------------------------- 1 | from target import TargetType 2 | import os 3 | import re 4 | import cv2 5 | import time 6 | import subprocess 7 | import numpy as np 8 | 9 | TARGET_DIR = './assets/' 10 | TMP_DIR = './tmp/' 11 | 12 | SIMILAR = { 13 | '1': ['i', 'I', 'l', '|', ':', '!', '/', '\\'], 14 | '2': ['z', 'Z'], 15 | '3': [], 16 | '4': [], 17 | '5': ['s', 'S'], 18 | '6': [], 19 | '7': [], 20 | '8': ['&'], 21 | '9': [], 22 | '0': ['o', 'O', 'c', 'C', 'D'] 23 | } 24 | 25 | 26 | class UIMatcher: 27 | @staticmethod 28 | def match(screen, target: TargetType): 29 | """ 30 | 在指定快照中确定货物的屏幕位置。 31 | """ 32 | # 获取对应货物的图片。 33 | # 有个要点:通过截屏制作货物图片时,请在快照为实际大小的模式下截屏。 34 | template = cv2.imread(target.value) 35 | # 获取货物图片的宽高。 36 | th, tw = template.shape[:2] 37 | 38 | # 调用 OpenCV 模板匹配。 39 | res = cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED) 40 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) 41 | rank = max_val 42 | 43 | # 矩形左上角的位置。 44 | tl = max_loc 45 | 46 | # 阈值判断。 47 | if rank < 0.82: 48 | return None 49 | 50 | # 这里,我随机加入了数字(15),用于补偿匹配值和真实位置的差异。 51 | return tl[0] + tw / 2 + 15, tl[1] + th / 2 + 15, rank 52 | 53 | @staticmethod 54 | def read(filepath: str): 55 | """ 56 | 工具函数,用于读取图片。 57 | """ 58 | return cv2.imread(filepath) 59 | 60 | @staticmethod 61 | def write(image): 62 | """ 63 | 工具函数,用于读取图片。 64 | """ 65 | ts = str(int(time.time())) 66 | return cv2.imwrite(f'{TARGET_DIR}{ts}.jpg', image) 67 | 68 | @staticmethod 69 | def image_to_txt(image, cleanup=False, plus=''): 70 | # cleanup为True则识别完成后删除生成的文本文件 71 | # plus参数为给tesseract的附加高级参数 72 | image_url = f'{TMP_DIR}tmp.jpg' 73 | txt_name = f'{TMP_DIR}tmp' 74 | txt_url = f'{txt_name}.txt' 75 | if not os.path.exists(TMP_DIR): os.mkdir(TMP_DIR) 76 | cv2.imwrite(image_url, image) 77 | 78 | subprocess.check_output('tesseract --dpi 72 ' + image_url + ' ' + 79 | txt_name + ' ' + plus, shell=True) # 生成同名txt文件 80 | text = '' 81 | with open(txt_url, 'r') as f: 82 | text = f.read().strip() 83 | if cleanup: 84 | os.remove(txt_url) 85 | os.remove(image_url) 86 | return text 87 | 88 | @staticmethod 89 | def normalize_txt(txt: str): 90 | for key, sim_list in SIMILAR.items(): 91 | for sim in sim_list: 92 | txt = txt.replace(sim, key) 93 | txt = re.sub(r'\D', '', txt) 94 | return txt 95 | 96 | @staticmethod 97 | def cut(image, left_up, len_width=(190, 50)): 98 | sx = left_up[0] 99 | sy = left_up[1] 100 | dx = left_up[0] + len_width[0] 101 | dy = left_up[1] + len_width[1] 102 | return image[sy:dy, sx:dx] 103 | 104 | @staticmethod 105 | def plain(image): 106 | kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) 107 | erode = cv2.erode(image, kernel) 108 | dilate = cv2.dilate(erode, kernel) 109 | return dilate 110 | 111 | @staticmethod 112 | def fill_color(image): 113 | copy_image = image.copy() 114 | h, w = image.shape[:2] 115 | mask = np.zeros([h + 2, w + 2], np.uint8) 116 | cv2.floodFill(copy_image, mask, (0, 0), (255, 255, 255), (100, 100, 100), (50, 50, 50), 117 | cv2.FLOODFILL_FIXED_RANGE) 118 | return copy_image 119 | 120 | @staticmethod 121 | def pre(image): 122 | image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) 123 | lower_blue = np.array([103, 43, 46]) 124 | upper_blue = np.array([103, 255, 255]) 125 | image = cv2.inRange(image, lower_blue, upper_blue) 126 | return image 127 | 128 | @staticmethod 129 | def pre_building_panel(image): 130 | image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) 131 | mask_orange = cv2.inRange(image, (10, 40, 40), (40, 255, 255)) 132 | mask_blue = cv2.inRange(image, (80, 40, 40), (140, 255, 255)) 133 | image = cv2.bitwise_or(mask_orange, mask_blue) 134 | return image 135 | -------------------------------------------------------------------------------- /prop.py: -------------------------------------------------------------------------------- 1 | from building import BuildingType 2 | from target import TargetType 3 | 4 | # 坐标相关 5 | # 每个建筑等级显示的左上角坐标 6 | BUILDING_LEVEL_POS = { 7 | 1: (219, 1205), 8 | 2: (469, 1078), 9 | 3: (719, 950), 10 | 4: (216, 947), 11 | 5: (467, 822), 12 | 6: (728, 697), 13 | 7: (215, 696), 14 | 8: (468, 570), 15 | 9: (725, 445) 16 | } 17 | # 建筑坐标 18 | BUILDING_POS = { 19 | 1: (294, 1184), 20 | 2: (551, 1061), 21 | 3: (807, 961), 22 | 4: (275, 935), 23 | 5: (535, 810), 24 | 6: (799, 687), 25 | 7: (304, 681), 26 | 8: (541, 568), 27 | 9: (787, 447) 28 | } 29 | # 查看每个建筑等级的按钮坐标 30 | BUILDING_DETAIL_BTN = (982, 1151) 31 | # 建筑升级按钮坐标 32 | BUILDING_UPGRADE_BTN = (863, 1703) 33 | 34 | # 建筑中心-建筑详细面板中 35 | # 等级数字左上角坐标 36 | BUILDING_INFO_PANEL_LEVEL_POS = (790, 500) 37 | # 升级按钮坐标 38 | BUILDING_INFO_PANEL_UPGRADE_BTN = (540, 1390) 39 | 40 | # 导航栏建设按钮坐标 41 | CONSTRUCT_BTN = (190, 1800) 42 | 43 | # 商店面板的各种坐标 44 | # 导航栏商店按钮坐标 保证能点到商店 45 | SHOP_BTN = (420, 1820) 46 | # 红包标题栏中间坐标 47 | REDPACKET_TITLE_POS = (530, 350) 48 | # 各按钮打开两字附近的坐标 49 | # 福气红包/小红包按钮坐标 50 | REDPACKET_BTN_S = (200, 850) 51 | # 多福红包/中红包按钮坐标 52 | REDPACKET_BTN_M = (530, 850) 53 | # 满福红包/大红包按钮坐标 54 | REDPACKET_BTN_L = (870, 850) 55 | # 相册按钮坐标 56 | ALBUM_BTN = (530, 1530) 57 | 58 | # 游戏属性相关 59 | # 建筑对应加buff的对象 60 | BUFF_PAIR = { 61 | BuildingType.木屋: [BuildingType.木材厂], 62 | BuildingType.居民楼: [BuildingType.便利店], 63 | BuildingType.钢结构房: [BuildingType.钢铁厂], 64 | BuildingType.平房: [], 65 | BuildingType.小型公寓: [], 66 | BuildingType.人才公寓: [], 67 | BuildingType.花园洋房: [BuildingType.商贸中心], 68 | BuildingType.中式小楼: [], 69 | BuildingType.空中别墅: [BuildingType.民食斋], 70 | BuildingType.复兴公馆: [], 71 | BuildingType.便利店: [BuildingType.居民楼], 72 | BuildingType.五金店: [BuildingType.零件厂], 73 | BuildingType.服装店: [BuildingType.纺织厂], 74 | BuildingType.菜市场: [BuildingType.食品厂], 75 | BuildingType.学校: [BuildingType.图书城], 76 | BuildingType.图书城: [BuildingType.学校, BuildingType.造纸厂], 77 | BuildingType.商贸中心: [BuildingType.花园洋房], 78 | BuildingType.加油站: [BuildingType.人民石油], 79 | BuildingType.民食斋: [BuildingType.空中别墅], 80 | BuildingType.媒体之声: [], 81 | BuildingType.木材厂: [BuildingType.木屋], 82 | BuildingType.食品厂: [BuildingType.菜市场], 83 | BuildingType.造纸厂: [BuildingType.图书城], 84 | BuildingType.水厂: [], 85 | BuildingType.电厂: [], 86 | BuildingType.钢铁厂: [BuildingType.钢结构房], 87 | BuildingType.纺织厂: [BuildingType.服装店], 88 | BuildingType.零件厂: [BuildingType.五金店], 89 | BuildingType.企鹅机械: [BuildingType.零件厂], 90 | BuildingType.人民石油: [BuildingType.加油站], 91 | BuildingType.梦想公寓: [BuildingType.游泳馆, BuildingType.强国煤业], 92 | BuildingType.追梦快递: [], 93 | BuildingType.游泳馆: [BuildingType.梦想公寓], 94 | BuildingType.强国煤业: [BuildingType.梦想公寓], 95 | } 96 | # 建筑品质 97 | _RANK_NORMAL = 0 98 | _RANK_RARE = 1 99 | _RANK_EPIC = 2 100 | BUILDING_RANK = { 101 | BuildingType.木屋: _RANK_NORMAL, 102 | BuildingType.居民楼: _RANK_NORMAL, 103 | BuildingType.钢结构房: _RANK_NORMAL, 104 | BuildingType.平房: _RANK_NORMAL, 105 | BuildingType.小型公寓: _RANK_NORMAL, 106 | BuildingType.人才公寓: _RANK_RARE, 107 | BuildingType.花园洋房: _RANK_RARE, 108 | BuildingType.中式小楼: _RANK_RARE, 109 | BuildingType.空中别墅: _RANK_EPIC, 110 | BuildingType.复兴公馆: _RANK_EPIC, 111 | BuildingType.便利店: _RANK_NORMAL, 112 | BuildingType.五金店: _RANK_NORMAL, 113 | BuildingType.服装店: _RANK_NORMAL, 114 | BuildingType.菜市场: _RANK_NORMAL, 115 | BuildingType.学校: _RANK_NORMAL, 116 | BuildingType.图书城: _RANK_RARE, 117 | BuildingType.商贸中心: _RANK_RARE, 118 | BuildingType.加油站: _RANK_RARE, 119 | BuildingType.民食斋: _RANK_EPIC, 120 | BuildingType.媒体之声: _RANK_EPIC, 121 | BuildingType.木材厂: _RANK_NORMAL, 122 | BuildingType.食品厂: _RANK_NORMAL, 123 | BuildingType.造纸厂: _RANK_NORMAL, 124 | BuildingType.水厂: _RANK_NORMAL, 125 | BuildingType.电厂: _RANK_NORMAL, 126 | BuildingType.钢铁厂: _RANK_RARE, 127 | BuildingType.纺织厂: _RANK_RARE, 128 | BuildingType.零件厂: _RANK_RARE, 129 | BuildingType.企鹅机械: _RANK_EPIC, 130 | BuildingType.人民石油: _RANK_EPIC, 131 | BuildingType.梦想公寓: _RANK_EPIC, 132 | BuildingType.追梦快递: _RANK_RARE, 133 | BuildingType.游泳馆: _RANK_EPIC, 134 | BuildingType.强国煤业: _RANK_EPIC, 135 | } 136 | # 建筑对应火车货物 137 | BUILDING_2_GOODS = { 138 | BuildingType.木屋: TargetType.Chair, 139 | BuildingType.居民楼: TargetType.Box, 140 | BuildingType.钢结构房: TargetType.Sofa, 141 | BuildingType.平房: TargetType.Potting, 142 | BuildingType.小型公寓: TargetType.Cupboard, 143 | BuildingType.人才公寓: TargetType.Computer, 144 | BuildingType.花园洋房: TargetType.Fertilizer, 145 | BuildingType.中式小楼: TargetType.Quilt, 146 | BuildingType.空中别墅: TargetType.Lamp, 147 | BuildingType.复兴公馆: TargetType.Silk, 148 | BuildingType.便利店: TargetType.Bottle, 149 | BuildingType.五金店: TargetType.Screw, 150 | BuildingType.服装店: TargetType.Cloth, 151 | BuildingType.菜市场: TargetType.Vegetable, 152 | BuildingType.学校: TargetType.Schoolbag, 153 | BuildingType.图书城: TargetType.Book, 154 | BuildingType.商贸中心: TargetType.Heels, 155 | BuildingType.加油站: TargetType.Gasoline, 156 | BuildingType.民食斋: TargetType.Chicken, 157 | BuildingType.媒体之声: TargetType.Mic, 158 | BuildingType.木材厂: TargetType.Wood, 159 | BuildingType.食品厂: TargetType.Food, 160 | BuildingType.造纸厂: TargetType.Straw, 161 | BuildingType.水厂: TargetType.Chlorine, 162 | BuildingType.电厂: TargetType.Mine, 163 | BuildingType.钢铁厂: TargetType.Coal, 164 | BuildingType.纺织厂: TargetType.Cotton, 165 | BuildingType.零件厂: TargetType.Steel, 166 | BuildingType.企鹅机械: TargetType.Kit, 167 | BuildingType.人民石油: TargetType.Oil, 168 | BuildingType.梦想公寓: TargetType.Refrigerator, 169 | BuildingType.追梦快递: TargetType.FlatCar, 170 | BuildingType.游泳馆: TargetType.BeachUmbrella, 171 | BuildingType.强国煤业: TargetType.Helmet, 172 | } 173 | 174 | # 终止信号 175 | END = 'end' 176 | 177 | # 命令信号 178 | RUN = 'run' 179 | 180 | # 命令集 181 | # 升级至 x 级 182 | UPGRADE_TO = 'upgrade_to' 183 | # 升级 x 次 184 | UPGRADE_TIMES = 'upgrade_times' 185 | # 命令模式信号 186 | COMMAND_MODE = 'command_mode' 187 | # 开红包 188 | UNPACK = 'unpack' 189 | # 开相册 190 | OPEN_ALBUM = 'album' 191 | # 输出总结信息 192 | SUMMARY = 'summary' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | JGM Automator 3 |

4 | 5 |

6 | GitHub GitHub tag (latest by date) 家国梦支持版本 GitHub last commit 7 |

8 | 9 | > 该项目为`Python`编写的基于 OpenCV 模板匹配和 Tesseract 文字识别的《家国梦》游戏自动化脚本。 10 | 11 | ## 主要功能 12 | 13 | - :moneybag:定时收取金币 14 | - :package:自动运送货物 15 | - 指定运送货物的等级,可实现只给史诗建筑供货 16 | - :building_construction:定时升级建筑 17 | - 只升级指定建筑列表,适用于有主要产出建筑的情景 18 | - :building_construction:主动升级建筑 19 | - 升至 x 级 20 | - 升级 x 次 21 | - :love_letter:极速开红包相册 22 | - :steam_locomotive:自动重启游戏刷新火车 23 | - 仅限 QQ 账号登陆 24 | 25 | ## 特性 26 | - 将硬编码部分配置文件化 27 | - 支持配置文件的热加载 28 | - 支持在命令行中以回车暂停/重启,以及优雅关闭,方便手动操作(如抽奖) 29 | - 支持在命令行输入特定命令来主动执行某些操作 30 | 31 | ## 更新 32 | 33 | ### [`v1.1.1`](https://github.com/Dwayneten/JGM-Automator/releases/tag/v1.1.1) 34 | 35 | - 增加新版本4个货物图片和配置 36 | 37 | - 更新一些货物图片以提升识别率 38 | 39 | ### [`v1.1.0`](https://github.com/Dwayneten/JGM-Automator/releases/tag/v1.1.0) 40 | 41 | - 极速开红包/相册 大幅提升开红包/相册的速度 42 | 43 | - 增加输出总结信息的命令 44 | 45 | - 现在重启游戏约一分钟后仍未成功进入游戏界面则中止脚本 46 | 47 | - 修改了一下代码格式 48 | 49 | ## 计算器 50 | 51 | :computer:计算家国梦建筑最优摆放策略 52 | 53 | 推荐 [lintx](https://github.com/lintx/) 写的 [家国梦计算器](https://lintx.github.io/jgm-calculator/index.html) 54 | 55 | ## 导航 56 | 57 | - [安装与运行](#安装与运行) 58 | - [环境](#环境) 59 | - [依赖](#依赖) 60 | - [运行](#运行) 61 | - [操作说明](#操作说明) 62 | - [功能说明](#功能说明) 63 | - [主动升级建筑](#主动升级建筑) 64 | - [命令模式](#命令模式) 65 | - [自动开红包相册](#自动开红包相册) 66 | - [开发说明](#开发说明) 67 | - [开发计划](#开发计划) 68 | 69 | ## 安装与运行 70 | 71 | ### 环境 72 | 73 | - [Python 3.7](https://www.python.org/downloads/) 74 | - [ADB Kits](http://adbshell.com/downloads) 75 | - [MuMu 模拟器](https://mumu.163.com/) 76 | 77 | 安装完后将`Python` 和 `ADB`的目录添加到系统环境变量`PATH`中,百度有教程 78 | 79 | ### 依赖 80 | 81 | 安装必须的 python 库 82 | 83 | ```bash 84 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple uiautomator2 opencv-python 85 | ``` 86 | 87 | 如果需要使用以下命令则需要 [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) 并将其目录添加到系统环境变量`PATH`中 88 | 89 | - `run upgrade_to x` 90 | 91 | 测试时使用的是`Tesseract OCR v5.0.0-alpha` [下载页面](https://github.com/UB-Mannheim/tesseract/wiki) 92 | 93 | 所有功能均在`Windows 10` `QQ 登陆` `MuMu 模拟器`的环境下测试 94 | 95 | ### 运行 96 | 97 | ```bash 98 | # 打开 JGM-Automator 文件夹 99 | # shift + 右键点空白处 100 | # 选择“在此处打开 Powershell 窗口”或“在此处打开命令行窗口” 101 | # 按以下步骤操作或输入命令 102 | 103 | # 打开 MuMu 模拟器,确保屏幕大小为 1920(长) * 1080(宽) 104 | # adb 连接 105 | adb connect 127.0.0.1:7555 106 | 107 | # 获取 device 名称,并填写至 main.py 108 | # MuMu 模拟器忽略 109 | adb devices 110 | 111 | # 在已完成 adb 连接后,在手机安装 ATX 应用 112 | # 此步骤仅第一次运行需要 113 | python -m uiautomator2 init 114 | 115 | # 按需求配置 config.json 116 | 117 | # 打开模拟器中的 ATX ,点击“启动 UIAutomator”选项,确保 UIAutomator 是运行的。 118 | 119 | # 进入游戏界面,启动自动脚本。 120 | python main.py 121 | ``` 122 | 123 | ## 操作说明 124 | 125 | ### 建筑位置编号 126 | ```bash 127 | ###7#8#9# 128 | ##4#5#6## 129 | #1#2#3### 130 | ``` 131 | 132 | ### 配置文件 133 | 134 | - `config.json` 135 | 136 | ```JavaScript 137 | { 138 | "swipe_interval_sec": 5, // 收取金币最少间隔秒数 139 | "upgrade_interval_sec": 50, // 升级建筑最少间隔秒数 140 | "building_pos": [ 141 | ["零件厂", "人民石油", "企鹅机械"], 142 | ["商贸中心", "媒体之声", "民食斋"], 143 | ["花园洋房", "复兴公馆", "小型公寓"] 144 | ], // 排布与游戏界面一致,修改为自己场上建筑名称 145 | "train_get_rank": [0, 1, 2] // 要送货的货物品质 0-普通/1-稀有/2-史诗 只需送史诗则只留 [2] 146 | "debug_mode": false, // 调试模式 可无视 147 | "upgrade_building": true, // 是否开启自动升级建筑 true/false 148 | "upgrade_building_list": [7, 9], // 要升级的建筑位置编号 此例中先尝试升级 7 号再尝试升级 9 号 149 | "refresh_train": true, // 是否开启自动刷新火车 true/false 150 | "detect_goods": true // 是否开启自动送货 每天送完货后可关闭 true/false 151 | } 152 | ``` 153 | 154 | ### 命令行操作 155 | 156 | ```bash 157 | # 启动 158 | python main.py 159 | 160 | # 暂停/重启(会有日志提示) 161 | [回车] 162 | 163 | # 结束应用 164 | end[回车] 165 | ``` 166 | 167 | 以下命令均需暂停后输入才生效 168 | 169 | ```bash 170 | # 进入命令模式 171 | run command_mode on 172 | # 退出命令模式 173 | run command_mode off 174 | # 将该建筑升至 x 级 175 | run upgrade_to x 176 | # 将该建筑升级 x 次 177 | run upgrade_times x 178 | # 开小/福气红包 x 次 179 | run unpack s x 180 | # 开中/多福红包 x 次 181 | run unpack m x 182 | # 开大/满福红包 x 次 183 | run unpack l x 184 | # 开相册 x 次 185 | run album x 186 | # 输出总结信息 187 | run summary 188 | ``` 189 | 190 | ## 功能说明 191 | 192 | ### 主动升级建筑 193 | 194 | 1. 脚本启动并正常运行后按回车进入暂停模式 195 | 2. 打开建筑中心并进入目标建筑的详细面板 196 | - 输入`run upgrade_to x`并回车将该建筑升至 x 级 197 | - 输入`run upgrade_times x`并回车将该建筑升级 x 次 198 | 3. 操作完成后将自动返回主界面并继续运行常规流程 199 | 200 | ### 命令模式 201 | 202 | 1. 脚本启动并正常运行后按回车进入暂停模式 203 | 2. 输入`run command_mode on` 进入暂停模式 204 | 3. 在命令模式下可以执行多次命令而不返回常规流程 205 | 4. 输入`run command_mode off` 退出暂停模式并返回游戏主界面 206 | 207 | 在需要对多个建筑进行升级时可以进入命令模式 208 | 209 | 例:主力建筑已经1000+级,想将其他建筑提升至925级以获取等级奖励 210 | 211 | ```bash 212 | [回车] // 进入暂停模式 213 | run command_mode on // 进入命令模式 214 | // 手动在游戏内进入建筑中心并打开目标建筑的详细面板 215 | run upgrade_to 925 // 将其升至 925 级 216 | // 等待升级完成后打开下一木匾建筑的详细面板 217 | run upgrade_to 925 // 将其升至 925 级 218 | // 升完想升的建筑后 219 | run command_mode off // 退出命令模式自动返回主界面继续常规流程 220 | ``` 221 | ### 自动开红包相册 222 | 223 | 1. 脚本启动并正常运行后按回车进入暂停模式 224 | 2. 打开商店面板 225 | 3. 输入`run unpack m x`自动开 x 个多福红包 226 | 4. 其他类型的红包或相册同样步骤 227 | 228 | **注意**:为了保证开完一个红包会多点击几下,但不保证特殊情况下,如开满福红包三张卡都出史诗且三张卡同时升星时能点完这个红包 229 | 230 | ## 开发说明 231 | 232 | - [Weditor](https://github.com/openatx/weditor) 233 | 234 | 我们可以使用 Weditor 工具,获取屏幕坐标,以及在线编写自动化脚本。 235 | 236 | ```bash 237 | # 安装依赖 238 | python -m pip install --pre weditor 239 | 240 | # 启动 Weditor 241 | python -m weditor 242 | ``` 243 | 244 | ## 开发计划 245 | 246 | - [ ] 自动升级所有建筑到 x 级以获取奖励 247 | 248 | - [x] 开红包 249 | 250 | - [x] 自动重启游戏刷新火车 251 | 252 | - [ ] 供货阵容和产出阵容互换 253 | -------------------------------------------------------------------------------- /automator.py: -------------------------------------------------------------------------------- 1 | from target import TargetType 2 | from multiprocessing import Queue 3 | from config import Reader 4 | from cv import UIMatcher 5 | import uiautomator2 as u2 6 | import logging 7 | import time 8 | import prop 9 | 10 | BASIC_FORMAT = "[%(asctime)s] %(levelname)s - %(message)s" 11 | DATE_FORMAT = "%Y-%m-%d %H:%M:%S" 12 | formatter = logging.Formatter(BASIC_FORMAT, DATE_FORMAT) 13 | 14 | chlr = logging.StreamHandler() # 输出到控制台的handler 15 | chlr.setFormatter(formatter) 16 | 17 | fhlr = logging.FileHandler("logging.log") # 输出到文件的handler 18 | fhlr.setFormatter(formatter) 19 | 20 | logger = logging.getLogger() 21 | logger.setLevel('INFO') 22 | logger.addHandler(chlr) 23 | logger.addHandler(fhlr) 24 | 25 | class Automator: 26 | def __init__(self, device: str, keyboard: Queue): 27 | """ 28 | device: 如果是 USB 连接,则为 adb devices 的返回结果;如果是模拟器,则为模拟器的控制 URL 。 29 | """ 30 | self.d = u2.connect(device) 31 | self.config = Reader() 32 | self.upgrade_iter_round = 0 33 | self.keyboard = keyboard 34 | self.command_mode = False 35 | self._check_uiautomator() 36 | self.time_start_working = time.time() 37 | self.refresh_times = 0 38 | self.delivered_times = 0 39 | 40 | def _need_continue(self): 41 | if not self.keyboard.empty(): 42 | # 不在命令模式下时才接受回车暂停 43 | if not self.command_mode: 44 | txt = self.keyboard.get() 45 | if txt == prop.END: 46 | logger.info('End') 47 | return False 48 | logger.info('Pause') 49 | txt = self.keyboard.get() 50 | if txt == prop.END: 51 | logger.info('End') 52 | return False 53 | # 判断是否输入命令 54 | elif txt.split(' ')[0] == prop.RUN: 55 | # 若输入了命令则进行解析 56 | self._interpreter(txt.split(' ')[1:]) 57 | return True 58 | else: 59 | logger.info('Restart') 60 | return True 61 | else: 62 | return True 63 | 64 | def _interpreter(self, cmd): 65 | """ 66 | cmd: 用户输入的命令 67 | """ 68 | # logger.info(txt.split(' ')[1:]) 69 | op = cmd[0] 70 | # 命令 - 升至 x 级 71 | if op == prop.UPGRADE_TO: 72 | try: 73 | target_level = int(cmd[1]) 74 | except Exception: 75 | logger.warn("Invalid number. Ignored.") 76 | else: 77 | self._upgrade_to(target_level) 78 | # 命令 - 升级 x 次 79 | elif op == prop.UPGRADE_TIMES: 80 | try: 81 | input_num = int(cmd[1]) 82 | except Exception: 83 | logger.warn("Invalid number. Ignored.") 84 | else: 85 | self._upgrade_times(input_num) 86 | # 命令 - 命令模式 87 | elif op == prop.COMMAND_MODE: 88 | if len(cmd) == 2 and cmd[1] == 'on': 89 | self.command_mode = True 90 | logger.info('Enter command mode.') 91 | elif len(cmd) == 2 and cmd[1] == 'off': 92 | self.command_mode = False 93 | logger.info('Exit command mode.') 94 | self._return_main_area() 95 | else: 96 | logger.warn("Unknown parameter. Ignored.") 97 | # 命令 - 拆红包 98 | elif op == prop.UNPACK: 99 | if len(cmd) == 3 and cmd[1] in ['s', 'm', 'l']: 100 | try: 101 | input_num = int(cmd[2]) 102 | except Exception: 103 | logger.warn("Invalid number. Ignored.") 104 | else: 105 | self._unpack_times(cmd[1], input_num) 106 | logger.info('Unpack complete.') 107 | else: 108 | logger.warn("Unknown parameter. Ignored.") 109 | elif op == prop.OPEN_ALBUM: 110 | try: 111 | input_num = int(cmd[1]) 112 | except Exception: 113 | logger.warn("Invalid number. Ignored.") 114 | else: 115 | self._open_albums(input_num) 116 | logger.info('Open complete.') 117 | elif op == prop.SUMMARY: 118 | self._print_summary() 119 | # 无法识别命令 120 | else: 121 | logger.warn("Unknown command. Ignored.") 122 | if not self.command_mode: 123 | logger.info('Restart') 124 | 125 | def start(self): 126 | """ 127 | 启动脚本,请确保已进入游戏页面。 128 | """ 129 | tmp_upgrade_last_time = time.time() 130 | logger.info("Start Working") 131 | while True: 132 | # 检查是否有键盘事件 133 | if not self._need_continue(): 134 | break 135 | 136 | # 进入命令模式后不继续执行常规操作 137 | if self.command_mode: 138 | continue 139 | 140 | # 更新配置文件 141 | self.config.refresh() 142 | 143 | if self.config.debug_mode: 144 | logger.info("Debug mode") 145 | # 重启游戏法 146 | # self._refresh_train_by_restart() 147 | 148 | # 重连 wifi 法 149 | # self._refresh_train_by_reconnect() 150 | 151 | # 是否检测货物 152 | if self.config.detect_goods: 153 | logger.info('-' * 30) 154 | logger.info("Start matching goods") 155 | # 获取当前屏幕快照 156 | screen = self._safe_screenshot() 157 | # 判断是否出现货物。 158 | has_goods = False 159 | refresh_flag = False 160 | for target in self.config.goods_2_building_seq.keys(): 161 | has_goods |= self._match_target(screen, target) 162 | # 如果需要刷新火车并且已送过目标货物 163 | if has_goods and self.config.refresh_train: 164 | refresh_flag = True 165 | logger.info("All target goods delivered.") 166 | # 如果需要刷新火车并且未送过目标货物 167 | elif self.config.refresh_train: 168 | for target in self.config.goods_2_building_seq_excpet_target.keys(): 169 | if UIMatcher.match(screen, target) is not None: 170 | has_goods = True 171 | break 172 | if has_goods: 173 | refresh_flag = True 174 | logger.info("Train detected with no target goods.") 175 | else: 176 | logger.info("Train not detected.") 177 | if refresh_flag: 178 | # 刷新火车 179 | logger.info("Refresh train.") 180 | logger.info("-" * 30) 181 | self.refresh_times += 1 182 | if not self._refresh_train_by_restart(): 183 | # 重启不成功(超时)时中止脚本 184 | logger.warn("Timed out waiting for restart!") 185 | break 186 | else: 187 | logger.info("End matching") 188 | 189 | # 简单粗暴的方式,处理 “XX之光” 的荣誉显示。 190 | # 当然,也可以使用图像探测的模式。 191 | self.d.click(550, 1650) 192 | 193 | # 滑动屏幕,收割金币。 194 | logger.info("Collect coins") 195 | self._swipe() 196 | 197 | # 自动升级建筑 198 | tmp_upgrade_interval = time.time() - tmp_upgrade_last_time 199 | if tmp_upgrade_interval >= self.config.upgrade_interval_sec: 200 | if self.config.upgrade_building is True: 201 | self._auto_upgrade_building() 202 | tmp_upgrade_last_time = time.time() 203 | else: 204 | logger.info(f"Left {round(self.config.upgrade_interval_sec - tmp_upgrade_interval, 2)}s to upgrade") 205 | 206 | time.sleep(self.config.swipe_interval_sec) 207 | self._print_summary() 208 | logger.info('Sub process end') 209 | 210 | def _swipe(self): 211 | """ 212 | 滑动屏幕,收割金币。 213 | """ 214 | for i in range(3): 215 | # 横向滑动,共 3 次。 216 | sx, sy = self._get_position(i * 3 + 1) 217 | ex, ey = self._get_position(i * 3 + 3) 218 | self.d.swipe(sx, sy, ex, ey) 219 | 220 | @staticmethod 221 | def _get_position(key): 222 | """ 223 | 获取指定建筑的屏幕位置。 224 | 225 | ###7#8#9# 226 | ##4#5#6## 227 | #1#2#3### 228 | """ 229 | return prop.BUILDING_POS.get(key) 230 | 231 | def _get_target_position(self, target: TargetType): 232 | """ 233 | 获取货物要移动到的屏幕位置。 234 | """ 235 | return self._get_position(self.config.goods_2_building_seq.get(target)) 236 | 237 | def _match_target(self, screen, target: TargetType): 238 | """ 239 | 探测货物,并搬运货物。 240 | """ 241 | # 由于 OpenCV 的模板匹配有时会智障,故我们探测次数实现冗余。 242 | counter = 6 243 | logged = False 244 | while counter != 0: 245 | counter = counter - 1 246 | 247 | # 使用 OpenCV 探测货物。 248 | result = UIMatcher.match(screen, target) 249 | 250 | # 若无探测到,终止对该货物的探测。 251 | # 实现冗余的原因:返回的货物屏幕位置与实际位置存在偏差,导致移动失效 252 | if result is None: 253 | break 254 | 255 | rank = result[-1] 256 | result = result[:2] 257 | sx, sy = result 258 | # 获取货物目的地的屏幕位置。 259 | ex, ey = self._get_target_position(target) 260 | 261 | if not logged: 262 | self.delivered_times += 1 263 | logger.info(f"Detect {target} at ({sx},{sy}), rank: {rank}") 264 | logged = True 265 | 266 | # 搬运货物。 267 | self.d.swipe(sx, sy, ex, ey) 268 | # 侧面反映检测出货物 269 | return logged 270 | 271 | def _auto_upgrade_building(self): 272 | """ 273 | 按顺序升级建筑 274 | """ 275 | logger.info("Start upgrade buildings") 276 | self.d.click(*prop.BUILDING_DETAIL_BTN) 277 | time.sleep(0.5) 278 | for pos in self.config.upgrade_building_list: 279 | self.d.click(*self._get_position(pos)) 280 | time.sleep(0.5) 281 | self.d.click(*prop.BUILDING_UPGRADE_BTN) 282 | time.sleep(0.5) 283 | self.d.click(*prop.BUILDING_DETAIL_BTN) 284 | logger.info("Upgrade complete") 285 | 286 | def _upgrade_to(self, target_level): 287 | """ 288 | target_level: 目标等级 289 | 升至 target_level 级 290 | 利用 Tesseract 识别当前等级后点击升级按钮 target_level - 当前等级次 291 | """ 292 | screen = self._safe_screenshot() 293 | screen = UIMatcher.pre_building_panel(screen) 294 | tmp = UIMatcher.cut(screen, prop.BUILDING_INFO_PANEL_LEVEL_POS, (120, 50)) 295 | # import cv2 296 | # cv2.imwrite("./tmp/screen.jpg", screen) 297 | tmp = UIMatcher.plain(tmp) 298 | tmp = UIMatcher.fill_color(tmp) 299 | tmp = UIMatcher.plain(tmp) 300 | txt = UIMatcher.image_to_txt(tmp, plus='-l chi_sim --psm 7') 301 | txt = UIMatcher.normalize_txt(txt) 302 | try: 303 | cur_level = int(txt) 304 | logger.info(f'Current level -> {cur_level}') 305 | except Exception: 306 | logger.warning(f'Current level -> {txt}') 307 | return 308 | click_times = target_level - cur_level 309 | self._upgrade_times(click_times) 310 | 311 | def _upgrade_times(self, click_times: int): 312 | """ 313 | click_times: 点击/升级次数 314 | 执行点击升级按钮的操作 click_times 次 315 | """ 316 | # assert(times >= 0) 317 | while click_times > 0: 318 | click_times -= 1 319 | bx, by = prop.BUILDING_INFO_PANEL_UPGRADE_BTN 320 | self.d.click(bx, by) 321 | time.sleep(0.015) 322 | logger.info("Upgrade complete") 323 | # 非命令模式下完成操作后返回主界面以继续常规流程 324 | if not self.command_mode: 325 | self._return_main_area() 326 | 327 | def _return_main_area(self): 328 | """ 329 | 通过点击两次导航栏内建设按钮来回到主界面 330 | """ 331 | time.sleep(0.5) 332 | tx, ty = prop.CONSTRUCT_BTN 333 | self.d.click(tx, ty) 334 | time.sleep(0.1) 335 | self.d.click(tx, ty) 336 | time.sleep(0.5) 337 | 338 | def _unpack_times(self, pack_type, num: int): 339 | """ 340 | 开红包 num 个 341 | """ 342 | # 红包标题栏坐标 开红包后点这里直到开完这个红包 343 | tx, ty = prop.REDPACKET_TITLE_POS 344 | if pack_type == 'm': 345 | bx, by = prop.REDPACKET_BTN_M 346 | t = 12 347 | elif pack_type == 'l': 348 | bx, by = prop.REDPACKET_BTN_L 349 | t = 24 350 | else: 351 | bx, by = prop.REDPACKET_BTN_S 352 | t = 6 353 | self.d.click(bx, by) 354 | time.sleep(1) 355 | while num > 1: 356 | num -= 1 357 | self.d.press("enter") 358 | time.sleep(0.08) 359 | time.sleep(1) 360 | # 防止意外多点几下 例如升星或开出史诗 361 | for _ in range(t): 362 | self.d.click(tx, ty) 363 | time.sleep(0.25) 364 | if not self.command_mode: 365 | self._return_main_area() 366 | 367 | def _open_albums(self, num: int): 368 | """ 369 | 开相册 num 个 370 | """ 371 | self.d.click(*prop.ALBUM_BTN) 372 | time.sleep(1) 373 | while num > 1: 374 | num -= 1 375 | self.d.press("enter") 376 | time.sleep(0.08) 377 | time.sleep(1) 378 | for _ in range(4): 379 | self.d.click(*prop.REDPACKET_TITLE_POS) 380 | time.sleep(0.5) 381 | if not self.command_mode: 382 | self._return_main_area() 383 | 384 | def _is_good_to_go(self): 385 | """ 386 | 检测是否有排行图标来判断是否进入了游戏界面 387 | """ 388 | screen = self._safe_screenshot() 389 | return UIMatcher.match(screen, TargetType.Rank_btn) is not None 390 | 391 | def _refresh_train_by_restart(self): 392 | """ 393 | 通过重启游戏的方法来刷新火车 394 | 全程用时大约在 20s 左右 395 | qq 账号测试不用授权 20s 左右 396 | """ 397 | time_before_restart = time.time() 398 | self.d.app_stop("com.tencent.jgm") 399 | self.d.app_start("com.tencent.jgm", activity=".MainActivity") 400 | time.sleep(5) 401 | good_to_go = False 402 | try_times = 0 403 | while not good_to_go: 404 | try_times += 1 405 | if self._is_good_to_go(): 406 | good_to_go = True 407 | logger.info(f"Refresh train costs {round(time.time() - time_before_restart, 2)}s.") 408 | elif try_times >= 60: 409 | return False 410 | else: 411 | time.sleep(1) 412 | return True 413 | 414 | def _refresh_train_by_reconnect(self): 415 | """ 416 | 通过关闭开启 wifi 的方法刷新火车 417 | 要重新登陆+授权 暂时弃用 418 | """ 419 | self.d.press("home") 420 | time.sleep(0.5) 421 | logger.info("Wifi disable.") 422 | logger.info(self.d.adb_shell("svc wifi disable")) 423 | time.sleep(0.5) 424 | logger.info("Wifi enable.") 425 | logger.info(self.d.adb_shell("svc wifi enable")) 426 | time.sleep(5) 427 | self.d.app_start("com.tencent.jgm", activity=".MainActivity") 428 | 429 | def _check_uiautomator(self): 430 | """ 431 | 检查 uiautomator 运行状态 432 | """ 433 | if not self.d.uiautomator.running(): 434 | self.d.reset_uiautomator() 435 | 436 | def _safe_screenshot(self): 437 | """ 438 | 防止执行 screenshot 时报错终止 439 | """ 440 | self._check_uiautomator() 441 | return self.d.screenshot(format="opencv") 442 | 443 | def _print_summary(self): 444 | logger.info('-' * 30) 445 | pass_time = time.time() - self.time_start_working 446 | logger.info(f"本次启动运行了 {int(pass_time // 3600)} 小时 {int(pass_time % 3600 // 60)} 分钟 {round(pass_time % 60, 2)} 秒") 447 | logger.info(f"重启了 {self.refresh_times} 次, 检测到 {self.delivered_times} 车厢目标货物(非总送货次数)") 448 | logger.info('-' * 30) 449 | --------------------------------------------------------------------------------