Repository: wangshub/wechat_jump_game Branch: master Commit: 3cbbb0795790 Files: 66 Total size: 99.1 KB Directory structure: gitextract_rovqql80/ ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── README.rst ├── Tools/ │ └── README.md ├── changelog.md ├── common/ │ ├── UnicodeStreamFilter.py │ ├── __init__.py │ ├── ai.py │ ├── auto_adb.py │ ├── config.py │ ├── debug.py │ └── screenshot.py ├── config/ │ ├── 1280x720/ │ │ └── config.json │ ├── 1440x720/ │ │ └── config.json │ ├── 1800x1080/ │ │ └── config.json │ ├── 1920x1080/ │ │ └── config.json │ ├── 2160x1080/ │ │ └── config.json │ ├── 2560x1440/ │ │ └── config.json │ ├── 720x1280 │ ├── 960x540/ │ │ └── config.json │ ├── default.json │ ├── huawei/ │ │ ├── honorV8 │ │ └── honor_note8_config.json │ ├── iPhone/ │ │ ├── 6_config.json │ │ ├── 8P_7P_6sP_6P_config.json │ │ ├── 8_config.json │ │ ├── SE_config.json │ │ └── X_config.json │ ├── mi/ │ │ ├── max2_config.json │ │ ├── mi5_config.json │ │ ├── mi5s_config.json │ │ ├── mi5x_config.json │ │ ├── mi6_config.json │ │ ├── mix2_config.json │ │ └── note2_config.json │ ├── samsung/ │ │ ├── s7edge_config.json │ │ ├── s8.json │ │ └── s8在设置里关闭曲面侧屏 │ └── smartisan/ │ └── pro2_config.json ├── jump_bot/ │ ├── Makefile │ ├── README.rst │ ├── jumpbot/ │ │ ├── algos.py │ │ ├── auto.py │ │ ├── bot.py │ │ ├── connector.py │ │ ├── manual.py │ │ └── settings.py │ └── requirements.txt ├── requirements.txt ├── resource/ │ └── model/ │ ├── checkpoint │ ├── model.ckpt.data-00000-of-00001 │ ├── model.ckpt.index │ └── model.ckpt.meta ├── wechat_jump.py ├── wechat_jump_auto.py ├── wechat_jump_auto_ai.py ├── wechat_jump_auto_curves.py ├── wechat_jump_auto_iOS.py ├── wechat_jump_auto_slim.py ├── wechat_jump_iOS_py3.py └── wechat_jump_py3.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CONTRIBUTING.md ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ 程序版本号:x.x.x ********** Screen: Physical size: x Density: Physical density: x Override density: x Device: x Phone OS: x Host OS: x Python: x ********** 遇到问题: 改进和建议:无 ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ 本次 PR 主要做的事情: - x 修改后最高分数:x ================================================ FILE: .gitignore ================================================ # user-defined test/ pic/ screenshot_backups/ autojump.png autojump_temp.png region*.png jump_range.csv # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python .idea build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log .static_storage/ .media/ local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # for mac .DS_store ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [wangshub] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ run: mkdir -p jumpbot/data python3 jumpbot/bot.py ================================================ FILE: README.md ================================================ # 教你用 Python 来玩微信跳一跳 [![GitHub stars](https://img.shields.io/github/stars/wangshub/wechat_jump_game.svg)](https://github.com/wangshub/wechat_jump_game/stargazers) [![GitHub forks](https://img.shields.io/github/forks/wangshub/wechat_jump_game.svg)](https://github.com/wangshub/wechat_jump_game/network) [![GitHub license](https://img.shields.io/github/license/wangshub/wechat_jump_game.svg)](https://github.com/wangshub/wechat_jump_game/blob/master/LICENSE) [![Throughput Graph](https://graphs.waffle.io/wangshub/wechat_jump_game/throughput.svg)](https://waffle.io/wangshub/wechat_jump_game/metrics/throughput) ## 游戏模式 > 2017 年 12 月 28 日下午,微信发布了 6.6.1 版本,加入了「小游戏」功能,并提供了官方 DEMO「跳一跳」。这是一个 2.5D 插画风格的益智游戏,玩家可以通过按压屏幕时间的长短来控制这个「小人」跳跃的距离。分数越高,那么在好友排行榜更加靠前。通过 Python 脚本自动运行,让你轻松霸榜。 ![](./resource/image/jump.gif) 可能刚开始上手的时候,因为时间距离之间的关系把握不恰当,只能跳出几个就掉到了台子下面。**如果能利用图像识别精确测量出起始和目标点之间测距离,就可以估计按压的时间来精确跳跃。** ## 原理说明 ##### 由于微信检测非常严厉,这里的防禁代码可能已经不起作用,主要供学习用途 1. 将手机点击到《跳一跳》小程序界面 2. 用 ADB 工具获取当前手机截图,并用 ADB 将截图 pull 上来 ```shell adb shell screencap -p /sdcard/autojump.png adb pull /sdcard/autojump.png . ``` 3. 计算按压时间 * 手动版:用 Matplotlib 显示截图,用鼠标先点击起始点位置,然后点击目标位置,计算像素距离; * 自动版:靠棋子的颜色来识别棋子,靠底色和方块的色差来识别棋盘; 4. 用 ADB 工具点击屏幕蓄力一跳 ```shell adb shell input swipe x y x y time(ms) ``` ## 使用教程 相关软件工具安装和使用步骤请参考 [Android 和 iOS 操作步骤](https://github.com/wangshub/wechat_jump_game/wiki/Android-%E5%92%8C-iOS-%E6%93%8D%E4%BD%9C%E6%AD%A5%E9%AA%A4) #### 获取源码 ``` - git clone https://github.com/wangshub/wechat_jump_game.git ``` ##### 非常推荐使用Python3,避免编码及import问题 ## PR 要求 ##### 请选择 merge 进 master 分支,并且标题写上简短描述,例子 [优化] 使用PEP8优化代码 ## 版本说明 - master 分支:稳定版本,已通过测试 - dev 分支:开发版本,包含一些较稳定的新功能,累计多个功能并测试通过后合并至 prod 分支 - 其他分支:功能开发 (feature) 或问题修复 (bugfix),属于最新尝鲜版本,可能处于开发中的状态,基本完成后合并至 dev 分支 ## FAQ - 详见 [Wiki-FAQ](https://github.com/wangshub/wechat_jump_game/wiki/FAQ) ## 更新日志 - 详见 [changelog](https://github.com/wangshub/wechat_jump_game/blob/master/changelog.md) ## 开发者列表 - 详见 [contributors](https://github.com/wangshub/wechat_jump_game/graphs/contributors) ## 交流 - 314659953 (1000 人) - 176740763 (500 人) - 或者关注我的微信公众号后台留言 ![](./resource/image/qrcode_for_gh_3586401957c4_258.jpg) ================================================ FILE: README.rst ================================================ ############################################################################## Wechat Jump Bot (iOS) ############################################################################## ============================================================================== Features ============================================================================== - Auto Mode: play the game automatically; - Manual Mode: play the game manually. Wechat Jump Game .. image:: https://github.com/alpesis-ai/wechat-jumpbot/blob/master/images/auto.png :height: 1334px :width: 500px ============================================================================== How it runs ============================================================================== Prerequisites - WebDriverAgent - libimobiledevice - Python 3 WebDriverAgent :: $ git clone https://github.com/facebook/WebDriverAgent && cd WebDriverAgent $ brew install carthage $ ./Scripts/bootstrap.sh # open WebDriverAgent.xcodeproj with Xcode # Xcode: # - code sign (general and build_settings): WebDriverAgentLib/WebDriverAgentRunner # - Product -> Destination -> # - Product -> Scheme -> WebDriverAgentRunner # - Product -> Test libimobiledevice (iproxy) :: $ brew install libimobiledevice $ iproxy 8100 8100 # browse: http://localhost:8100/status # browse: http://localhost:8100/inspector Bot Agent (iOS) :: $ git clone https://github.com/alpesis-ai/wechat-jumpbot.git $ cd bot-agent-ios $ pip3 install --pre facebook-wda $ pip3 install -r requirements.txt # make run # - model: [ip, plus, ipx, se] # - mode: [auto, manual] # python3 jumpbot/bot.py --model --mode $ mkdir -p jumpbot/data # iphone 6/7 $ python3 jumpbot/bot.py --model ip --mode auto # iphone 6/7 plus $ python3 jumpbot/bot.py --model plus --mode auto # iphone X $ python3 jumpbot/bot.py --model ipx --mode auto # iphone SE $ python3 jumpbot/bot.py --model se --mode auto ============================================================================== Algorithms ============================================================================== Manual Mode: - click the piece(x, y) and board(x, y) and get the coordinates correspondingly - calculating the distance and press time :: (coord1[0][0] - coord2[0][0])**2 + (coord2[0][1] - coord2[0][1])**2 distance = distance ** 0.5 press_time = distance * settings.TIME_COEFF Auto Mode: - the main idea same as the manual mode, but detecting the piece and the board automatically - find coord_y_start_scan - find piece - find board ============================================================================== Developement ============================================================================== :: connector ---| | --> auto / manual --> bot algos ---| ================================================ FILE: Tools/README.md ================================================ ## 所有实验都在该文件夹下运行即可,已放上测试用的代码。免去配置 adb 的麻烦 - 复制 wechat_jump_game 根目录下的 config 文件夹以及 wechat_jump_py3.py 文件到本目录下 - 按住 `shift` + 右键 选择在该文件夹下打开命令窗口(Windows 用户请自行 cmd) - 打开安卓手机的 usb 调试,并连接电脑,在终端输入 `adb devices` 进行测试,如果有连接设备号则表示成功 - 打开微信小游戏,然后运行代码 `python wechat_jump_py3.py`,点击出现的图形起点和终点,棋子自动跳转 **注意:这里使用的是不需要配置的 adb 方式,需要在该文件下操作,至于如何自动跳转,只需改变执行脚本即可,这里只做演示** ================================================ FILE: changelog.md ================================================ ## 更新日志 - 2018-1-8 - 发布 release 一键启动 app V1.1.2 版本,针对腾讯 ban 方法进行反 ban 操作,详见 [STOP_jump](https://github.com/wangshub/wechat_jump_game/releases) - 2018-1-3 : - 发布 release 一键启动 app,详见 [STOP_jump](https://github.com/wangshub/wechat_jump_game/releases) - 2017-12-30 : - 请将安卓手机的 USB 调试模式打开,设置 > 更多设置 > 开发者选项 > USB 调试,如果出现运行脚本后小人不跳的情况,请检查是否有打开 “USB 调试(安全模式)” - 根据大家反馈:1080 屏幕距离系数 **1.393**,2K 屏幕为 **1** - 添加部分机型配置文件,可直接复制使用 - 2017-12-29 : - 增加更新自动化运行脚本,感谢 GitHub 上的 [@binderclip](https://github.com/binderclip) ================================================ FILE: common/UnicodeStreamFilter.py ================================================ # -*- coding: utf-8 -*- import sys if sys.version_info.major != 3: class UnicodeStreamFilter: def __init__(self, target): self.target = target self.encoding = 'utf-8' self.errors = 'replace' self.encode_to = self.target.encoding def write(self, s): if type(s) == str: s = s.decode("utf-8") s = s.encode(self.encode_to, self.errors).decode(self.encode_to) self.target.write(s) if sys.stdout.encoding == 'cp936': sys.stdout = UnicodeStreamFilter(sys.stdout) else: pass ================================================ FILE: common/__init__.py ================================================ ================================================ FILE: common/ai.py ================================================ # coding: utf-8 # Copyright (c) 2018 BeiTown import os import pandas from sklearn.linear_model import LinearRegression def linear_model_main(_distances, _press_times, target_distance): regr = LinearRegression() regr.fit(_distances, _press_times) predict_press_time = regr.predict(target_distance) result = {} # 截距 b result['intercept'] = regr.intercept_ # 斜率值 k result['coefficient'] = regr.coef_ # 预估的按压时间 result['value'] = predict_press_time return result def computing_k_b_v(target_distance): result = linear_model_main(distances, press_times, target_distance) b = result['intercept'] k = result['coefficient'] v = result['value'] return k[0], b[0], v[0] def add_data(distance, press_time): distances.append([distance]) press_times.append([press_time]) save_data('./jump_range.csv', distances, press_times) def save_data(file_name, distances, press_times): pf = pandas.DataFrame({'Distance': distances, 'Press_time': press_times}) # print(pf) pf.to_csv(file_name, index=False, sep=',') def get_data(file_name): data = pandas.read_csv(file_name) distance_array = [] press_time_array = [] for distance, press_time in zip(data['Distance'], data['Press_time']): distance_array.append([float(distance.strip().strip('[]'))]) press_time_array.append([float(press_time.strip().strip('[]'))]) return distance_array, press_time_array def init(): global distances, press_times distances = [] press_times = [] if os.path.exists('./jump_range.csv'): distances, press_times = get_data('./jump_range.csv') else: save_data('./jump_range.csv', [], []) return 0 def get_result_len(): return len(distances) ================================================ FILE: common/auto_adb.py ================================================ # -*- coding: utf-8 -*- import os import subprocess import platform class auto_adb(): def __init__(self): try: adb_path = 'adb' subprocess.Popen([adb_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.adb_path = adb_path except OSError: if platform.system() == 'Windows': adb_path = os.path.join('Tools', "adb", 'adb.exe') try: subprocess.Popen( [adb_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.adb_path = adb_path except OSError: pass else: try: subprocess.Popen( [adb_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError: pass print('请安装 ADB 及驱动并配置环境变量') print('具体链接: https://github.com/wangshub/wechat_jump_game/wiki') exit(1) def get_screen(self): process = os.popen(self.adb_path + ' shell wm size') output = process.read() return output def run(self, raw_command): command = '{} {}'.format(self.adb_path, raw_command) process = os.popen(command) output = process.read() return output def test_device(self): print('检查设备是否连接...') command_list = [self.adb_path, 'devices'] process = subprocess.Popen(command_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = process.communicate() if output[0].decode('utf8') == 'List of devices attached\n\n': print('未找到设备') print('adb 输出:') for each in output: print(each.decode('utf8')) exit(1) print('设备已连接') print('adb 输出:') for each in output: print(each.decode('utf8')) def test_density(self): process = os.popen(self.adb_path + ' shell wm density') output = process.read() return output def test_device_detail(self): process = os.popen(self.adb_path + ' shell getprop ro.product.device') output = process.read() return output def test_device_os(self): process = os.popen(self.adb_path + ' shell getprop ro.build.version.release') output = process.read() return output def adb_path(self): return self.adb_path ================================================ FILE: common/config.py ================================================ # -*- coding: utf-8 -*- """ 调取配置文件和屏幕分辨率的代码 """ import os import sys import json import re from common.auto_adb import auto_adb adb = auto_adb() def open_accordant_config(): """ 调用配置文件 """ screen_size = _get_screen_size() config_file = "{path}/config/{screen_size}/config.json".format( path=sys.path[0], screen_size=screen_size ) # 优先获取执行文件目录的配置文件 here = sys.path[0] for file in os.listdir(here): if re.match(r'(.+)\.json', file): file_name = os.path.join(here, file) with open(file_name, 'r') as f: print("Load config file from {}".format(file_name)) return json.load(f) # 根据分辨率查找配置文件 if os.path.exists(config_file): with open(config_file, 'r') as f: print("正在从 {} 加载配置文件".format(config_file)) return json.load(f) else: with open('{}/config/default.json'.format(sys.path[0]), 'r') as f: print("Load default config") return json.load(f) def _get_screen_size(): """ 获取手机屏幕大小 """ size_str = adb.get_screen() m = re.search(r'(\d+)x(\d+)', size_str) if m: return "{height}x{width}".format(height=m.group(2), width=m.group(1)) return "1920x1080" ================================================ FILE: common/debug.py ================================================ # -*- coding: utf-8 -*- """ 这是debug的代码,当DEBUG_SWITCH开关开启的时候,会将各种信息存在本地,方便检查故障 """ import os import sys import shutil import math from PIL import ImageDraw import platform if platform.system() == 'Windows': os.chdir(os.getcwd().replace('\\common', '')) path_split = "\\" else: os.chdir(os.getcwd().replace('/common', '')) path_split = '/' # from common import ai try: from common.auto_adb import auto_adb except ImportError as ex: print(ex) print('请将脚本放在项目根目录中运行') print('请检查项目根目录中的 common 文件夹是否存在') exit(1) screenshot_backup_dir = 'screenshot_backups' adb = auto_adb() def make_debug_dir(screenshot_backup_dir): """ 创建备份文件夹 """ if not os.path.isdir(screenshot_backup_dir): os.mkdir(screenshot_backup_dir) def backup_screenshot(ts): """ 为了方便失败的时候 debug """ make_debug_dir(screenshot_backup_dir) shutil.copy('{}{}autojump.png'.format(os.getcwd(), path_split), os.path.join(os.getcwd(), screenshot_backup_dir, str(ts) + '.png')) def save_debug_screenshot(ts, im, piece_x, piece_y, board_x, board_y): """ 对 debug 图片加上详细的注释 """ make_debug_dir(screenshot_backup_dir) draw = ImageDraw.Draw(im) draw.line((piece_x, piece_y) + (board_x, board_y), fill=2, width=3) draw.line((piece_x, 0, piece_x, im.size[1]), fill=(255, 0, 0)) draw.line((0, piece_y, im.size[0], piece_y), fill=(255, 0, 0)) draw.line((board_x, 0, board_x, im.size[1]), fill=(0, 0, 255)) draw.line((0, board_y, im.size[0], board_y), fill=(0, 0, 255)) draw.ellipse((piece_x - 10, piece_y - 10, piece_x + 10, piece_y + 10), fill=(255, 0, 0)) draw.ellipse((board_x - 10, board_y - 10, board_x + 10, board_y + 10), fill=(0, 0, 255)) del draw im.save(os.path.join(os.getcwd(), screenshot_backup_dir, '#' + str(ts) + '.png')) def computing_error(last_press_time, target_board_x, target_board_y, last_piece_x, last_piece_y, temp_piece_x, temp_piece_y): """ 计算跳跃实际误差 """ target_distance = math.sqrt( (target_board_x - last_piece_x) ** 2 + (target_board_y - last_piece_y) ** 2) # 上一轮目标跳跃距离 actual_distance = math.sqrt((temp_piece_x - last_piece_x) ** 2 + (temp_piece_y - last_piece_y) ** 2) # 上一轮实际跳跃距离 jump_error_value = math.sqrt((target_board_x - temp_piece_x) ** 2 + (target_board_y - temp_piece_y) ** 2) # 跳跃误差 print(round(target_distance), round(jump_error_value), round(actual_distance), round(last_press_time)) ''''# 将结果采集进学习字典 if last_piece_x > 0 and last_press_time > 0: ai.add_data(round(actual_distance, 2), round(last_press_time)) # print(round(actual_distance), round(last_press_time))''' def dump_device_info(): """ 显示设备信息 """ size_str = adb.get_screen() device_str = adb.test_device_detail() phone_os_str = adb.test_device_os() density_str = adb.test_density() print("""********** Screen: {size} Density: {dpi} Device: {device} Phone OS: {phone_os} Host OS: {host_os} Python: {python} **********""".format( size=size_str.replace('\n', ''), dpi=density_str.replace('\n', ''), device=device_str.replace('\n', ''), phone_os=phone_os_str.replace('\n', ''), host_os=sys.platform, python=sys.version )) ================================================ FILE: common/screenshot.py ================================================ # -*- coding: utf-8 -*- """ 手机屏幕截图的代码 """ import subprocess import os import sys from PIL import Image from io import StringIO try: from common.auto_adb import auto_adb except Exception as ex: print(ex) print('请将脚本放在项目根目录中运行') print('请检查项目根目录中的 common 文件夹是否存在') exit(1) adb = auto_adb() # SCREENSHOT_WAY 是截图方法,经过 check_screenshot 后,会自动递减,不需手动修改 SCREENSHOT_WAY = 3 def pull_screenshot(): """ 获取屏幕截图,目前有 0 1 2 3 四种方法,未来添加新的平台监测方法时, 可根据效率及适用性由高到低排序 """ global SCREENSHOT_WAY if 1 <= SCREENSHOT_WAY <= 3: process = subprocess.Popen( adb.adb_path + ' shell screencap -p', shell=True, stdout=subprocess.PIPE) binary_screenshot = process.stdout.read() if SCREENSHOT_WAY == 2: binary_screenshot = binary_screenshot.replace(b'\r\n', b'\n') elif SCREENSHOT_WAY == 1: binary_screenshot = binary_screenshot.replace(b'\r\r\n', b'\n') return Image.open(StringIO(binary_screenshot)) elif SCREENSHOT_WAY == 0: adb.run('shell screencap -p /sdcard/autojump.png') adb.run('pull /sdcard/autojump.png .') return Image.open('./autojump.png') def check_screenshot(): """ 检查获取截图的方式 """ global SCREENSHOT_WAY if os.path.isfile('autojump.png'): try: os.remove('autojump.png') except Exception: pass if SCREENSHOT_WAY < 0: print('暂不支持当前设备') sys.exit() try: im = pull_screenshot() im.load() im.close() print('采用方式 {} 获取截图'.format(SCREENSHOT_WAY)) except Exception: SCREENSHOT_WAY -= 1 check_screenshot() ================================================ FILE: config/1280x720/config.json ================================================ { "under_game_score_y": 200, "press_coefficient": 2.099, "piece_base_height_1_2": 13, "piece_body_width": 47, "swipe": { "x1": 374, "y1": 1060, "x2": 374, "y2": 1060 } } ================================================ FILE: config/1440x720/config.json ================================================ { "under_game_score_y": 200, "press_coefficient": 2.099, "piece_base_height_1_2": 13, "piece_body_width": 47, "swipe" : { "x1": 360, "y1": 1140, "x2": 360, "y2": 1142 } } ================================================ FILE: config/1800x1080/config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.2, "piece_base_height_1_2": 20, "piece_body_width": 70, "swipe": { "x1": 500, "y1": 1600, "x2": 500, "y2": 1602 } } ================================================ FILE: config/1920x1080/config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.392, "piece_base_height_1_2": 20, "piece_body_width": 70, "head_diameter": 60, "swipe": { "x1": 500, "y1": 1600, "x2": 500, "y2": 1602 } } ================================================ FILE: config/2160x1080/config.json ================================================ { "under_game_score_y": 420, "press_coefficient": 1.372, "piece_base_height_1_2": 25, "piece_body_width": 85 } ================================================ FILE: config/2560x1440/config.json ================================================ { "under_game_score_y": 410, "press_coefficient": 1.475, "piece_base_height_1_2": 28, "piece_body_width": 110, "swipe": { "x1": 320, "y1": 410, "x2": 320, "y2": 410 } } ================================================ FILE: config/720x1280 ================================================ { "under_game_score_y": 369, "press_coefficient": 2.003, "piece_base_height_1_2": 13, "piece_body_width": 45, "swipe" : { "x1": 374, "y1": 1060, "x2": 374, "y2": 1060 } } ================================================ FILE: config/960x540/config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 2.732, "piece_base_height_1_2": 20, "piece_body_width": 70 } ================================================ FILE: config/default.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.392, "piece_base_height_1_2": 20, "piece_body_width": 70 } ================================================ FILE: config/huawei/honorV8 ================================================ { "under_game_score_y": 400, "press_coefficient": 1.07, "piece_base_height_1_2": 90, "piece_body_width": 120, "swipe": { "x1": 730, "y1": 2100, "x2": 720, "y2": 2100 } } ================================================ FILE: config/huawei/honor_note8_config.json ================================================ { "under_game_score_y": 400, "press_coefficient": 1.04, "piece_base_height_1_2": 90, "piece_body_width": 120, "swipe": { "x1": 730, "y1": 2100, "x2": 720, "y2": 2100 } } ================================================ FILE: config/iPhone/6_config.json ================================================ { "under_game_score_y": 200, "press_coefficient": 1.95, "piece_base_height_1_2": 13, "piece_body_width": 49, "swipe": { "x1": 375, "y1": 1055, "x2": 375, "y2": 1055 } } ================================================ FILE: config/iPhone/8P_7P_6sP_6P_config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.2, "piece_base_height_1_2": 20, "piece_body_width": 70, "swipe": { "x1": 320, "y1": 410, "x2": 320, "y2": 410 } } ================================================ FILE: config/iPhone/8_config.json ================================================ { "under_game_score_y": 200, "press_coefficient": 1.97, "piece_base_height_1_2": 13, "piece_body_width": 50, "swipe": { "x1": 375, "y1": 1200, "x2": 375, "y2": 1200 } } ================================================ FILE: config/iPhone/SE_config.json ================================================ { "under_game_score_y": 190, "press_coefficient": 2.3, "piece_base_height_1_2": 12, "piece_body_width": 50, "swipe" : { "x1": 375, "y1": 1055, "x2": 375, "y2": 1055 } } ================================================ FILE: config/iPhone/X_config.json ================================================ { "under_game_score_y": 170, "press_coefficient": 1.31, "piece_base_height_1_2": 23, "piece_body_width": 70, "swipe": { "x1": 320, "y1": 410, "x2": 320, "y2": 410 } } ================================================ FILE: config/mi/max2_config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.5, "piece_base_height_1_2": 20, "piece_body_width": 70, "swipe": { "x1": 500, "y1": 1600, "x2": 500, "y2": 1600 } } ================================================ FILE: config/mi/mi5_config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.475, "piece_base_height_1_2": 20, "piece_body_width": 70, "swipe": { "x1": 540, "y1": 1514, "x2": 540, "y2": 1514 } } ================================================ FILE: config/mi/mi5s_config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.475, "piece_base_height_1_2": 20, "piece_body_width": 70, "swipe": { "x1": 540, "y1": 1514, "x2": 540, "y2": 1514 } } ================================================ FILE: config/mi/mi5x_config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.45, "piece_base_height_1_2": 25, "piece_body_width": 80, "swipe": { "x1": 560, "y1": 1550, "x2": 560, "y2": 1550 } } ================================================ FILE: config/mi/mi6_config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.45, "piece_base_height_1_2": 20, "piece_body_width": 70, "swipe": { "x1": 500, "y1": 1600, "x2": 500, "y2": 1600 } } ================================================ FILE: config/mi/mix2_config.json ================================================ { "under_game_score_y": 420, "press_coefficient": 1.480, "piece_base_height_1_2": 25, "piece_body_width": 85 } ================================================ FILE: config/mi/note2_config.json ================================================ { "under_game_score_y": 300, "press_coefficient": 1.47, "piece_base_height_1_2": 25, "piece_body_width": 80, "swipe": { "x1": 540, "y1": 1600, "x2": 540, "y2": 1600 } } ================================================ FILE: config/samsung/s7edge_config.json ================================================ { "under_game_score_y": 384, "press_coefficient": 1, "piece_base_height_1_2": 95, "piece_body_width": 102 } ================================================ FILE: config/samsung/s8.json ================================================ { "under_game_score_y": 460, "press_coefficient": 1.365, "piece_base_height_1_2": 70, "piece_body_width": 75 } ================================================ FILE: config/samsung/s8在设置里关闭曲面侧屏 ================================================ ================================================ FILE: config/smartisan/pro2_config.json ================================================ { "under_game_score_y": 411, "press_coefficient": 1.392, "piece_base_height_1_2": 20, "piece_body_width": 70, "swipe" : { "x1": 320, "y1": 410, "x2": 320, "y2": 410 } } ================================================ FILE: jump_bot/Makefile ================================================ run: mkdir -p jumpbot/data python3 jumpbot/bot.py ================================================ FILE: jump_bot/README.rst ================================================ ############################################################################## Wechat Jump Bot (iOS) ############################################################################## ============================================================================== Features ============================================================================== - Auto Mode: playing the game automatically; - Manual Mode: playing the game by manual. Wechat Jump Game .. image:: https://github.com/alpesis-ai/wechat-jumpbot/blob/master/images/auto.png :height: 1334px :width: 500px ============================================================================== How it runs ============================================================================== Prequsites - WebDriverAgent - libimobiledevice - Python 3 WebDriverAgent :: $ git clone https://github.com/facebook/WebDriverAgent && cd WebDriverAgent $ brew install carthage $ ./Scripts/bootstrap.sh # open WebDriverAgent.xcodeproj with Xcode # Xcode: # - code sign (general and build_settings): WebDriverAgentLib/WebDriverAgentRunner # - Product -> Destination -> # - Product -> Scheme -> WebDriverAgentRunner # - Product -> Test libimobiledevice (iproxy) :: $ brew install libimobiledevice $ iproxy 8100 8100 # browse: http://localhost:8100/status # browse: http://localhost:8100/inspector Bot Agent (iOS) :: $ git clone https://github.com/alpesis-ai/wechat-jumpbot.git $ cd bot-agent-ios $ pip3 install --pre facebook-wda $ pip3 install -r requirements.txt # make run # - model: [ip, plus, ipx, se] # - mode: [auto, manual] # python3 jumpbot/bot.py --model --mode $ mkdir -p jumpbot/data # iphone 6/7 $ python3 jumpbot/bot.py --model ip --mode auto # iphone 6/7 plus $ python3 jumpbot/bot.py --model plus --mode auto # iphone X $ python3 jumpbot/bot.py --model ipx --mode auto # iphone SE $ python3 jumpbot/bot.py --model se --mode auto ============================================================================== Algorithms ============================================================================== Manual Mode: - click the piece(x, y) and board(x, y) and get the coordinates correspondingly - calculating the distance and press time :: (coord1[0][0] - coord2[0][0])**2 + (coord2[0][1] - coord2[0][1])**2 distance = distance ** 0.5 press_time = distance * settings.TIME_COEFF Auto Mode: - the main idea same as the manual mode, but detecting the piece and the board automatically - find coord_y_start_scan - find piece - find board ============================================================================== Developement ============================================================================== :: connector ---| | --> auto / manual --> bot algos ---| ================================================ FILE: jump_bot/jumpbot/algos.py ================================================ import math def get_press_time(piece_x, piece_y, board_x, board_y, time_coeff): distance = math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2) press_time = distance * time_coeff / 1000 return press_time ================================================ FILE: jump_bot/jumpbot/auto.py ================================================ import time import math import random from PIL import Image, ImageDraw import settings from connector import Connector from algos import get_press_time class AutoBot(Connector): def __init__(self, params=settings.get_bot_params()): # init connector super(AutoBot, self).__init__() # init bot self.status = True # init game self.params = params self.swipe_x1 = 0; self.swipe_y1 = 0; self.swipe_x2 = 0; self.swipe_y2 = 0; def run(self): steps = 0 while (self.status): self.connector_screenshot() image = Image.open(self.image_dir) steps += 1 coord_y_start_scan = self._get_coord_y_start_scan(image) piece_x, piece_y = self._find_piece(image, coord_y_start_scan) board_x, board_y = self._find_board(image, piece_x, piece_y) print("step: ", steps) print("- image: ", image.size) print("- coord_y_start_scan: ", coord_y_start_scan) print("- piece (x, y): ", piece_x, piece_y) print("- board (x, y): ", board_x, board_y) if piece_x == 0: print("Game Over.") return self._set_button_coords(image) press_time = get_press_time(piece_x, piece_y, board_x, board_y, self.params["TIME_COEFF"]) print("- press time: ", press_time) self.connector_taphold(press_time) time.sleep(random.uniform(1, 1.1)) def _get_coord_y_start_scan(self, image): width, height = image.size pixels = image.load() coord_y_start_scan = 0 for i in range(self.params["COORD_Y_START_SCAN"], height, 50): last_pixel = pixels[0, i] for j in range(1, width): pixel = pixels[j, i] if (pixel[0] != last_pixel[0]) or (pixel[1] != last_pixel[1]) or (pixel[2] != last_pixel[2]): coord_y_start_scan = i - 50 break; if coord_y_start_scan: break; return coord_y_start_scan def _find_piece(self, image, coord_y_start_scan): width, height = image.size pixels = image.load() border_x_scan = int(width/8) piece_x_sum = 0 piece_x_counter = 0 piece_y_max = 0 for i in range(coord_y_start_scan, int(height * 2 / 3)): for j in range(border_x_scan, width - border_x_scan): pixel = pixels[j, i] if (50 < pixel[0] < 60) and (53 < pixel[1] < 63) and (95 < pixel[2] < 110): piece_x_sum += j piece_x_counter += 1 piece_y_max = max(i, piece_y_max) if not all((piece_x_sum, piece_x_counter)): return 0, 0 piece_x = piece_x_sum / piece_x_counter piece_y = piece_y_max - self.params["PIECE_BASE_HEIGHT_HALF"] return piece_x, piece_y def _find_board(self, image, piece_x, piece_y): width, height = image.size pixels = image.load() board_x = 0 board_y = 0 for i in range(int(height/3), int(height * 2/3)): if board_x or board_y: break; board_x_sum = 0 board_x_counter = 0 last_pixel = pixels[0, i] for j in range(width): pixel = pixels[j, i] if (abs(j - piece_x) < self.params["PIECE_BODY_WIDTH"]): continue if abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) > 10: board_x_sum += j board_x_counter += 1 if board_x_sum: board_x = board_x_sum / board_x_counter # find the centroid of next board board_y = piece_y - abs(board_x - piece_x) * math.sqrt(3) / 3 if not all ((board_x, board_y)): return 0, 0 return board_x, board_y def _set_button_coords(self, image): width, height = image.size left = width / 2 top = 1003 * (height / 1280.0) + 10 self.swipe_x1, self.swipe_y1, self.swipe_x2, self.swipe_y2 = left, top, left, top ================================================ FILE: jump_bot/jumpbot/bot.py ================================================ import argparse import settings from auto import AutoBot from manual import ManualBot def config(): parser = argparse.ArgumentParser(description='Wechat Jump Bot') parser.add_argument('-ml', '--model', type=str, help='device model = [ip, plus, ipx, se]', required=True) parser.add_argument('-me', '--mode', type=str, help='mode = [auto, manual]', required=True) return parser.parse_args() def jumpbot(parser): if parser.mode == "manual": bot = ManualBot(params=settings.get_bot_params(parser.model)) bot.run() elif parser.mode == "auto": bot = AutoBot(params=settings.get_bot_params(parser.model)) bot.run() else: print("ParamError: MODE should be ['auto', 'manual'].") if __name__ == '__main__': parser = config() jumpbot(parser) ================================================ FILE: jump_bot/jumpbot/connector.py ================================================ import wda import settings class Connector: def __init__(self, image_dir=settings.IMAGE_DIR): self.image_dir = image_dir self.client = wda.Client() self.session = self.client.session() def connector_screenshot(self): self.client.screenshot(self.image_dir) def connector_taphold(self, value): self.session.tap_hold(200, 200, value) ================================================ FILE: jump_bot/jumpbot/manual.py ================================================ import time import math import numpy as np from PIL import Image import matplotlib.pyplot as plt import matplotlib.animation as animation import settings from connector import Connector from algos import get_press_time class ManualBot(Connector): def __init__(self, params=settings.get_bot_params()): # init connector super(ManualBot, self).__init__() # init figure self.figure = plt.figure() # actions self.steps = 0 self.params = params self.coords = [] self.ix = [0, 0] self.iy = [0, 0] self.click_counter = 0 self.status = True def run(self): self.connector_screenshot() self.image = plt.imshow(self._read_image(), animated=True) self.action() def action(self): self.figure.canvas.mpl_connect('button_press_event', self._onclick) ani = animation.FuncAnimation(self.figure, self._update_figure, interval=50, blit=True) plt.show() def _onclick(self, event): coord = [] self.ix, self.iy = event.xdata, event.ydata coord.append((self.ix, self.iy)) print("coordinate = ", coord) self.coords.append(coord) self.click_counter += 1 if self.click_counter > 1: self.click_counter = 0 # next screen coord1 = self.coords.pop() coord2 = self.coords.pop() press_time = get_press_time(coord1[0][0], coord1[0][1], coord2[0][0], coord2[0][1], self.params["TIME_COEFF"]) self.steps += 1 print("Step: ", self.steps) print("- coord1: ", coord1) print("- coord2: ", coord2) print("- press_time: ", press_time) self.connector_taphold(press_time) self.status = True def _update_figure(self, *args): if self.status: time.sleep(1) self.connector_screenshot() self.image.set_array(self._read_image()) self.status = False return self.image, def _read_image(self): return np.array(Image.open(self.image_dir)) ================================================ FILE: jump_bot/jumpbot/settings.py ================================================ # Wechat Jump Bot (iOS) # ---------------------------------------------------------------------------- import os CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_ROOT = os.path.dirname(CURRENT_DIR) PROJECT_DIR = "jumpbot/" # ---------------------------------------------------------------------------- # Screenshot DATA_DIR = "data/" IMAGE = "screen.png" IMAGE_DIR = PROJECT_DIR + DATA_DIR + IMAGE # ---------------------------------------------------------------------------- # mode: ['auto', 'manual'] MODE = "manual" # ---------------------------------------------------------------------------- # Params def get_bot_params(model="ip"): bot_params = { "TIME_COEFF": 2., "COORD_Y_START_SCAN": 200, "PIECE_BASE_HEIGHT_HALF": 13, "PIECE_BODY_WIDTH": 49, "SWIPE_X1": 375, "SWIPE_Y1": 1055, "SWIPE_X2": 375, "SWIPE_Y2": 1055 } if model == "ip": bot_params["TIME_COEFF"] = 2. bot_params["COORD_Y_START_SCAN"] = 200 bot_params["PIECE_BASE_HEIGHT_HALF"] = 13 bot_params["PIECE_BODY_WIDTH"] = 49 bot_params["SWIPE_X1"] = 375 bot_params["SWIPE_Y1"] = 1055 bot_params["SWIPE_X2"] = 375 bot_params["SWIPE_Y2"] = 1055 elif model == "plus": bot_params["TIME_COEFF"] = 1.2 bot_params["COORD_Y_START_SCAN"] = 300 bot_params["PIECE_BASE_HEIGHT_HALF"] = 20 bot_params["PIECE_BODY_WIDTH"] = 70 bot_params["SWIPE_X1"] = 320 bot_params["SWIPE_Y1"] = 410 bot_params["SWIPE_X2"] = 320 bot_params["SWIPE_Y2"] = 410 elif model == "ipx": bot_params["TIME_COEFF"] = 1.31 bot_params["COORD_Y_START_SCAN"] = 170 bot_params["PIECE_BASE_HEIGHT_HALF"] = 23 bot_params["PIECE_BODY_WIDTH"] = 70 bot_params["SWIPE_X1"] = 320 bot_params["SWIPE_Y1"] = 410 bot_params["SWIPE_X2"] = 320 bot_params["SWIPE_Y2"] = 410 elif model == "se": bot_params["TIME_COEFF"] = 2.3 bot_params["COORD_Y_START_SCAN"] = 190 bot_params["PIECE_BASE_HEIGHT_HALF"] = 12 bot_params["PIECE_BODY_WIDTH"] = 50 bot_params["SWIPE_X1"] = 375 bot_params["SWIPE_Y1"] = 1055 bot_params["SWIPE_X2"] = 375 bot_params["SWIPE_Y2"] = 1055 else: print("ParamError: Unknown model type, model should be [ip, plus, ipx, se]") return bot_params ================================================ FILE: jump_bot/requirements.txt ================================================ backports.functools-lru-cache==1.4 cycler==0.10.0 matplotlib==2.1.1 numpy==1.13.3 olefile==0.44 opencv-python==3.4.0.12 Pillow==4.3.0 pyparsing==2.2.0 python-dateutil==2.6.1 pytz==2017.3 six==1.11.0 ================================================ FILE: requirements.txt ================================================ backports.functools-lru-cache==1.4 cycler==0.10.0 matplotlib==2.1.1 numpy==1.13.3 olefile==0.44 opencv-python==3.4.0.12 Pillow==4.3.0 pyparsing==2.2.0 python-dateutil==2.6.1 pytz==2017.3 six==1.11.0 tensorflow==1.4.0 pandas==0.22.0 scipy==1.0.0 scikit_learn==0.19.1 ================================================ FILE: resource/model/checkpoint ================================================ model_checkpoint_path: "model.ckpt" all_model_checkpoint_paths: "model.ckpt" ================================================ FILE: wechat_jump.py ================================================ # -*- coding: utf-8 -*- from __future__ import print_function, division import os import time import datetime import matplotlib.pyplot as plt import matplotlib.animation as animation import cv2 VERSION = "1.1.4" scale = 0.25 template = cv2.imread('./resource/image/character.png') template = cv2.resize(template, (0, 0), fx=scale, fy=scale) template_size = template.shape[:2] def search(img): result = cv2.matchTemplate(img, template, cv2.TM_SQDIFF) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) cv2.rectangle( img, (min_loc[0], min_loc[1]), (min_loc[0] + template_size[1], min_loc[1] + template_size[0]), (255, 0, 0), 4) return img, min_loc[0] + template_size[1] / 2, min_loc[1] + template_size[0] def pull_screenshot(): filename = datetime.datetime.now().strftime("%H%M%S") + '.png' os.system('mv autojump.png {}'.format(filename)) os.system('adb shell screencap -p /sdcard/autojump.png') os.system('adb pull /sdcard/autojump.png ./autojump.png') def jump(distance): press_time = distance * 1.35 press_time = int(press_time) cmd = 'adb shell input swipe 320 410 320 410 ' + str(press_time) print(cmd) os.system(cmd) def update_data(): global src_x, src_y img = cv2.imread('./autojump.png') img = cv2.resize(img, (0, 0), fx=scale, fy=scale) img, src_x, src_y = search(img) return img fig = plt.figure() pull_screenshot() img = update_data() im = plt.imshow(img, animated=True) update = True def updatefig(*args): global update if update: time.sleep(1) pull_screenshot() im.set_array(update_data()) update = False return im, def on_click(event): global update global src_x, src_y dst_x, dst_y = event.xdata, event.ydata distance = (dst_x - src_x)**2 + (dst_y - src_y)**2 distance = (distance ** 0.5) / scale print('distance = ', distance) jump(distance) update = True fig.canvas.mpl_connect('button_press_event', on_click) ani = animation.FuncAnimation(fig, updatefig, interval=5, blit=True) plt.show() ================================================ FILE: wechat_jump_auto.py ================================================ # -*- coding: utf-8 -*- """ === 思路 === 核心:每次落稳之后截图,根据截图算出棋子的坐标和下一个块顶面的中点坐标, 根据两个点的距离乘以一个时间系数获得长按的时间 识别棋子:靠棋子的颜色来识别位置,通过截图发现最下面一行大概是一条 直线,就从上往下一行一行遍历,比较颜色(颜色用了一个区间来比较) 找到最下面的那一行的所有点,然后求个中点,求好之后再让 Y 轴坐标 减小棋子底盘的一半高度从而得到中心点的坐标 识别棋盘:靠底色和方块的色差来做,从分数之下的位置开始,一行一行扫描, 由于圆形的块最顶上是一条线,方形的上面大概是一个点,所以就 用类似识别棋子的做法多识别了几个点求中点,这时候得到了块中点的 X 轴坐标,这时候假设现在棋子在当前块的中心,根据一个通过截图获取的 固定的角度来推出中点的 Y 坐标 最后:根据两点的坐标算距离乘以系数来获取长按时间(似乎可以直接用 X 轴距离) """ import math import re import random import sys import time from PIL import Image from six.moves import input if sys.version_info.major != 3: print('请使用Python3') exit(1) try: from common import debug, config, screenshot, UnicodeStreamFilter from common.auto_adb import auto_adb except Exception as ex: print(ex) print('请将脚本放在项目根目录中运行') print('请检查项目根目录中的 common 文件夹是否存在') exit(1) adb = auto_adb() VERSION = "1.1.4" # DEBUG 开关,需要调试的时候请改为 True,不需要调试的时候为 False DEBUG_SWITCH = False adb.test_device() # Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需 # 设置,设置保存在 config 文件夹中 config = config.open_accordant_config() under_game_score_y = config['under_game_score_y'] # 长按的时间系数,请自己根据实际情况调节 press_coefficient = config['press_coefficient'] # 二分之一的棋子底座高度,可能要调节 piece_base_height_1_2 = config['piece_base_height_1_2'] # 棋子的宽度,比截图中量到的稍微大一点比较安全,可能要调节 piece_body_width = config['piece_body_width'] # 图形中圆球的直径,可以利用系统自带画图工具,用直线测量像素,如果可以实现自动识别圆球直径,那么此处将可实现全自动。 head_diameter = config.get('head_diameter') if head_diameter == None: density_str = adb.test_density() matches = re.search(r'\d+', density_str) density_val = int(matches.group(0)) head_diameter = density_val / 8 def set_button_position(im): """ 将 swipe 设置为 `再来一局` 按钮的位置 """ global swipe_x1, swipe_y1, swipe_x2, swipe_y2 w, h = im.size left = int(w / 2) top = int(1584 * (h / 1920.0)) left = int(random.uniform(left - 200, left + 200)) top = int(random.uniform(top - 200, top + 200)) # 随机防 ban after_top = int(random.uniform(top - 200, top + 200)) after_left = int(random.uniform(left - 200, left + 200)) swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, after_left, after_top def jump(distance, delta_piece_y): """ 跳跃一定的距离 """ # 计算程序长度与截图测得的距离的比例 scale = 0.945 * 2 / head_diameter actual_distance = distance * scale * (math.sqrt(6) / 2) press_time = (-945 + math.sqrt(945 ** 2 + 4 * 105 * 36 * actual_distance)) / (2 * 105) * 1000 press_time *= press_coefficient press_time = max(press_time, 200) # 设置 200ms 是最小的按压时间 press_time = int(press_time) cmd = 'shell input swipe {x1} {y1} {x2} {y2} {duration}'.format( x1=swipe_x1, y1=swipe_y1, x2=swipe_x2, y2=swipe_y2, duration=press_time + delta_piece_y ) print(cmd) adb.run(cmd) return press_time def find_piece_and_board(im): """ 寻找关键坐标 """ w, h = im.size points = [] # 所有满足色素的点集合 piece_y_max = 0 board_x = 0 board_y = 0 scan_x_border = int(w / 8) # 扫描棋子时的左右边界 scan_start_y = 0 # 扫描的起始 y 坐标 im_pixel = im.load() # 以 50px 步长,尝试探测 scan_start_y for i in range(int(h / 3), int(h * 2 / 3), 50): last_pixel = im_pixel[0, i] for j in range(1, w): pixel = im_pixel[j, i] # 不是纯色的线,则记录 scan_start_y 的值,准备跳出循环 if pixel != last_pixel: scan_start_y = i - 50 break if scan_start_y: break print('start scan Y axis: {}'.format(scan_start_y)) # 从 scan_start_y 开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过 2/3 for i in range(scan_start_y, int(h * 2 / 3)): # 横坐标方面也减少了一部分扫描开销 for j in range(scan_x_border, w - scan_x_border): pixel = im_pixel[j, i] # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜 # 色这样应该 OK,暂时不提出来 if (50 < pixel[0] < 60) \ and (53 < pixel[1] < 63) \ and (95 < pixel[2] < 110): points.append((j, i)) piece_y_max = max(i, piece_y_max) bottom_x = [x for x, y in points if y == piece_y_max] # 所有最底层的点的横坐标 if not bottom_x: return 0, 0, 0, 0, 0 piece_x = int(sum(bottom_x) / len(bottom_x)) # 中间值 piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 # 限制棋盘扫描的横坐标,避免音符 bug if piece_x < w / 2: board_x_start = piece_x board_x_end = w else: board_x_start = 0 board_x_end = piece_x for i in range(int(h / 3), int(h * 2 / 3)): last_pixel = im_pixel[0, i] if board_x or board_y: break board_x_sum = 0 board_x_c = 0 for j in range(int(board_x_start), int(board_x_end)): pixel = im_pixel[j, i] # 修掉脑袋比下一个小格子还高的情况的 bug if abs(j - piece_x) < piece_body_width: continue # 检查Y轴下面5个像素, 和背景色相同, 那么是干扰 ver_pixel = im_pixel[j, i + 5] if abs(pixel[0] - last_pixel[0]) \ + abs(pixel[1] - last_pixel[1]) \ + abs(pixel[2] - last_pixel[2]) > 10 \ and abs(ver_pixel[0] - last_pixel[0]) \ + abs(ver_pixel[1] - last_pixel[1]) \ + abs(ver_pixel[2] - last_pixel[2]) > 10: board_x_sum += j board_x_c += 1 if board_x_sum: board_x = board_x_sum / board_x_c last_pixel = im_pixel[board_x, i] # 首先找到游戏的对称中心,由对称中心做辅助线与x=board_x直线的交点即为棋盘的中心位置 # 有了对称中心,可以知道棋子在棋盘上面的相对位置(偏高或偏低,偏高的话测量值比实际值大, # 偏低相反。最后通过delta_piece_y来对跳跃时间进行微调 center_x = w / 2 + (24 / 1080) * w center_y = h / 2 + (17 / 1920) * h if piece_x > center_x: board_y = round((25.5 / 43.5) * (board_x - center_x) + center_y) delta_piece_y = piece_y - round((25.5 / 43.5) * (piece_x - center_x) + center_y) else: board_y = round(-(25.5 / 43.5) * (board_x - center_x) + center_y) delta_piece_y = piece_y - round(-(25.5 / 43.5) * (piece_x - center_x) + center_y) if not all((board_x, board_y)): return 0, 0, 0, 0, 0 return piece_x, piece_y, board_x, board_y, delta_piece_y def yes_or_no(): """ 检查是否已经为启动程序做好了准备 """ while True: yes_or_no = str(input('请确保手机打开了 ADB 并连接了电脑,' '然后打开跳一跳并【开始游戏】后再用本程序,确定开始?[y/n]:')) if yes_or_no == 'y': break elif yes_or_no == 'n': print('谢谢使用', end='') exit(0) else: print('请重新输入') def main(): """ 主函数 """ print('程序版本号:{}'.format(VERSION)) print('激活窗口并按 CONTROL + C 组合键退出') debug.dump_device_info() screenshot.check_screenshot() i, next_rest, next_rest_time = (0, random.randrange(3, 10), random.randrange(5, 10)) while True: im = screenshot.pull_screenshot() # 获取棋子和 board 的位置 piece_x, piece_y, board_x, board_y, delta_piece_y = find_piece_and_board(im) ts = int(time.time()) print(ts, piece_x, piece_y, board_x, board_y) set_button_position(im) jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2), delta_piece_y) if DEBUG_SWITCH: debug.save_debug_screenshot(ts, im, piece_x, piece_y, board_x, board_y) debug.backup_screenshot(ts) im.close() i += 1 if i == next_rest: print('已经连续打了 {} 下,休息 {}秒'.format(i, next_rest_time)) for j in range(next_rest_time): sys.stdout.write('\r程序将在 {}秒 后继续'.format(next_rest_time - j)) sys.stdout.flush() time.sleep(1) print('\n继续') i, next_rest, next_rest_time = (0, random.randrange(30, 100), random.randrange(10, 60)) # 为了保证截图的时候应落稳了,多延迟一会儿,随机值防 ban time.sleep(random.uniform(1.2, 1.4)) if __name__ == '__main__': try: yes_or_no() main() except KeyboardInterrupt: adb.run('kill-server') print('\n谢谢使用', end='') exit(0) ================================================ FILE: wechat_jump_auto_ai.py ================================================ # coding: utf-8 ''' # === 思路 === # 核心:每次落稳之后截图,根据截图算出棋子的坐标和下一个块顶面的中点坐标, # 根据两个点的距离乘以一个时间系数获得长按的时间 # 识别棋子:靠棋子的颜色来识别位置,通过截图发现最下面一行大概是一条直线,就从上往下一行一行遍历, # 比较颜色(颜色用了一个区间来比较)找到最下面的那一行的所有点,然后求个中点, # 求好之后再让 Y 轴坐标减小棋子底盘的一半高度从而得到中心点的坐标 # 识别棋盘:靠底色和方块的色差来做,从分数之下的位置开始,一行一行扫描,由于圆形的块最顶上是一条线, # 方形的上面大概是一个点,所以就用类似识别棋子的做法多识别了几个点求中点, # 这时候得到了块中点的 X 轴坐标,这时候假设现在棋子在当前块的中心, # 根据一个通过截图获取的固定的角度来推出中点的 Y 坐标 # 最后:根据两点的坐标算距离乘以系数来获取长按时间(似乎可以直接用 X 轴距离) ''' import os import sys import subprocess import time import math import pandas from PIL import Image import random from six.moves import input try: from common import ai, debug, config, UnicodeStreamFilter from common.auto_adb import auto_adb except Exception as ex: print(ex) print('请将脚本放在项目根目录中运行') print('请检查项目根目录中的 common 文件夹是否存在') exit(1) adb = auto_adb() VERSION = "1.1.4" debug_switch = True # debug 开关,需要调试的时候请改为:True config = config.open_accordant_config() # Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需设置,设置保存在 config 文件夹中 under_game_score_y = config['under_game_score_y'] press_coefficient = config['press_coefficient'] # 长按的时间系数,请自己根据实际情况调节 piece_base_height_1_2 = config['piece_base_height_1_2'] # 二分之一的棋子底座高度,可能要调节 piece_body_width = config['piece_body_width'] # 棋子的宽度,比截图中量到的稍微大一点比较安全,可能要调节 screenshot_way = 2 def pull_screenshot(): process = subprocess.Popen('adb shell screencap -p', shell=True, stdout=subprocess.PIPE) screenshot = process.stdout.read() if sys.platform == 'win32': screenshot = screenshot.replace(b'\r\n', b'\n') f = open('autojump.png', 'wb') f.write(screenshot) f.close() def pull_screenshot_temp(): process = subprocess.Popen('adb shell screencap -p', shell=True, stdout=subprocess.PIPE) screenshot = process.stdout.read() if sys.platform == 'win32': screenshot = screenshot.replace(b'\r\n', b'\n') f = open('autojump_temp.png', 'wb') f.write(screenshot) f.close() def set_button_position(im): """ 将 swipe 设置为 `再来一局` 按钮的位置 """ global swipe_x1, swipe_y1, swipe_x2, swipe_y2 w, h = im.size left = int(w / 2) top = int(1584 * (h / 1920.0)) left = int(random.uniform(left - 100, left + 100)) top = int(random.uniform(top - 100, top + 100)) # 随机防 ban after_top = int(random.uniform(top - 100, top + 100)) after_left = int(random.uniform(left - 100, left + 100)) swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, after_left, after_top def jump(distance): ''' 跳跃一定的距离 ''' if ai.get_result_len() >= 10: # 需采集10条样本以上 k, b, v = ai.computing_k_b_v(distance) press_time = distance * k[0] + b print('Y = {k} * X + {b}'.format(k=k[0], b=b)) else: press_time = distance * press_coefficient press_time = max(press_time, 200) # 设置 200ms 是最小的按压时间 press_time = int(press_time) cmd = 'shell input swipe {x1} {y1} {x2} {y2} {duration}'.format( x1=swipe_x1, y1=swipe_y1, x2=swipe_x2, y2=swipe_y2, duration=press_time ) print('{}'.format(cmd)) adb.run(cmd) return press_time # 转换色彩模式hsv2rgb def hsv2rgb(h, s, v): h = float(h) s = float(s) v = float(v) h60 = h / 60.0 h60f = math.floor(h60) hi = int(h60f) % 6 f = h60 - h60f p = v * (1 - s) q = v * (1 - f * s) t = v * (1 - (1 - f) * s) r, g, b = 0, 0, 0 if hi == 0: r, g, b = v, t, p elif hi == 1: r, g, b = q, v, p elif hi == 2: r, g, b = p, v, t elif hi == 3: r, g, b = p, q, v elif hi == 4: r, g, b = t, p, v elif hi == 5: r, g, b = v, p, q r, g, b = int(r * 255), int(g * 255), int(b * 255) return r, g, b # 转换色彩模式rgb2hsv def rgb2hsv(r, g, b): r, g, b = r / 255.0, g / 255.0, b / 255.0 mx = max(r, g, b) mn = min(r, g, b) df = mx - mn if mx == mn: h = 0 elif mx == r: h = (60 * ((g - b) / df) + 360) % 360 elif mx == g: h = (60 * ((b - r) / df) + 120) % 360 elif mx == b: h = (60 * ((r - g) / df) + 240) % 360 if mx == 0: s = 0 else: s = df / mx v = mx return h, s, v def find_piece(im): ''' 寻找关键坐标 ''' w, h = im.size piece_x_sum = 0 piece_x_c = 0 piece_y_max = 0 scan_x_border = int(w / 8) # 扫描棋子时的左右边界 scan_start_y = 0 # 扫描的起始 y 坐标 im_pixel = im.load() # 以 50px 步长,尝试探测 scan_start_y for i in range(int(h / 3), int(h * 2 / 3), 50): last_pixel = im_pixel[0, i] for j in range(1, w): pixel = im_pixel[j, i] # 不是纯色的线,则记录 scan_start_y 的值,准备跳出循环 if pixel[0] != last_pixel[0] or pixel[1] != last_pixel[1] or pixel[2] != last_pixel[2]: scan_start_y = i - 50 break if scan_start_y: break # print('scan_start_y: {}'.format(scan_start_y)) # 从 scan_start_y 开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过 2/3 for i in range(scan_start_y, int(h * 2 / 3)): for j in range(scan_x_border, w - scan_x_border): # 横坐标方面也减少了一部分扫描开销 pixel = im_pixel[j, i] # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜色这样应该 OK,暂时不提出来 if (50 < pixel[0] < 60) and (53 < pixel[1] < 63) and (95 < pixel[2] < 110): piece_x_sum += j piece_x_c += 1 piece_y_max = max(i, piece_y_max) if not all((piece_x_sum, piece_x_c)): return 0, 0, piece_x = int(piece_x_sum / piece_x_c) piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 return piece_x, piece_y def find_piece_and_board(im): w, h = im.size piece_x_sum = 0 piece_x_c = 0 piece_y_max = 0 board_x = 0 board_y = 0 left_value = 0 left_count = 0 right_value = 0 right_count = 0 from_left_find_board_y = 0 from_right_find_board_y = 0 scan_x_border = int(w / 8) # 扫描棋子时的左右边界 scan_start_y = 0 # 扫描的起始y坐标 im_pixel = im.load() # 以50px步长,尝试探测scan_start_y for i in range(int(h / 3), int(h * 2 / 3), 50): last_pixel = im_pixel[0, i] for j in range(1, w): pixel = im_pixel[j, i] # 不是纯色的线,则记录scan_start_y的值,准备跳出循环 if pixel[0] != last_pixel[0] or pixel[1] != last_pixel[1] or pixel[2] != last_pixel[2]: scan_start_y = i - 50 break if scan_start_y: break # print('scan_start_y: ', scan_start_y) # 从scan_start_y开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过2/3 for i in range(scan_start_y, int(h * 2 / 3)): for j in range(scan_x_border, w - scan_x_border): # 横坐标方面也减少了一部分扫描开销 pixel = im_pixel[j, i] # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜色这样应该 OK,暂时不提出来 if (50 < pixel[0] < 60) and (53 < pixel[1] < 63) and (95 < pixel[2] < 110): piece_x_sum += j piece_x_c += 1 piece_y_max = max(i, piece_y_max) if not all((piece_x_sum, piece_x_c)): return 0, 0, 0, 0 piece_x = piece_x_sum / piece_x_c piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 for i in range(int(h / 3), int(h * 2 / 3)): last_pixel = im_pixel[0, i] # 计算阴影的RGB值,通过photoshop观察,阴影部分其实就是背景色的明度V 乘以0.7的样子 h, s, v = rgb2hsv(last_pixel[0], last_pixel[1], last_pixel[2]) r, g, b = hsv2rgb(h, s, v * 0.7) if from_left_find_board_y and from_right_find_board_y: break if not board_x: board_x_sum = 0 board_x_c = 0 for j in range(w): pixel = im_pixel[j, i] # 修掉脑袋比下一个小格子还高的情况的 bug if abs(j - piece_x) < piece_body_width: continue # 修掉圆顶的时候一条线导致的小 bug,这个颜色判断应该 OK,暂时不提出来 if abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) > 10: board_x_sum += j board_x_c += 1 if board_x_sum: board_x = board_x_sum / board_x_c else: # 继续往下查找,从左到右扫描,找到第一个与背景颜色不同的像素点,记录位置 # 当有连续3个相同的记录时,表示发现了一条直线 # 这条直线即为目标board的左边缘 # 然后当前的 y 值减 3 获得左边缘的第一个像素 # 就是顶部的左边顶点 for j in range(w): pixel = im_pixel[j, i] # 修掉脑袋比下一个小格子还高的情况的 bug if abs(j - piece_x) < piece_body_width: continue if (abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) > 10) and (abs(pixel[0] - r) + abs(pixel[1] - g) + abs(pixel[2] - b) > 10): if left_value == j: left_count = left_count + 1 else: left_value = j left_count = 1 if left_count > 3: from_left_find_board_y = i - 3 break # 逻辑跟上面类似,但是方向从右向左 # 当有遮挡时,只会有一边有遮挡 # 算出来两个必然有一个是对的 for j in range(w)[::-1]: pixel = im_pixel[j, i] # 修掉脑袋比下一个小格子还高的情况的 bug if abs(j - piece_x) < piece_body_width: continue if (abs(pixel[0] - last_pixel[0]) + abs(pixel[1] - last_pixel[1]) + abs(pixel[2] - last_pixel[2]) > 10) and (abs(pixel[0] - r) + abs(pixel[1] - g) + abs(pixel[2] - b) > 10): if right_value == j: right_count = left_count + 1 else: right_value = j right_count = 1 if right_count > 3: from_right_find_board_y = i - 3 break # 如果顶部像素比较多,说明图案近圆形,相应的求出来的值需要增大,这里暂定增大顶部宽的三分之一 if board_x_c > 5: from_left_find_board_y = from_left_find_board_y + board_x_c / 3 from_right_find_board_y = from_right_find_board_y + board_x_c / 3 # 按实际的角度来算,找到接近下一个 board 中心的坐标 这里的角度应该是30°,值应该是tan 30°,math.sqrt(3) / 3 board_y = piece_y - abs(board_x - piece_x) * math.sqrt(3) / 3 # 从左从右取出两个数据进行对比,选出来更接近原来老算法的那个值 if abs(board_y - from_left_find_board_y) > abs(from_right_find_board_y): new_board_y = from_right_find_board_y else: new_board_y = from_left_find_board_y if not all((board_x, board_y)): return 0, 0, 0, 0 return piece_x, piece_y, board_x, new_board_y def check_screenshot(): ''' 检查获取截图的方式 ''' global screenshot_way if os.path.isfile('autojump.png'): os.remove('autojump.png') if (screenshot_way < 0): print('暂不支持当前设备') sys.exit() pull_screenshot() try: Image.open('./autojump.png').load() print('采用方式 {} 获取截图'.format(screenshot_way)) except Exception: screenshot_way -= 1 check_screenshot() def yes_or_no(prompt, true_value='y', false_value='n', default=True): default_value = true_value if default else false_value prompt = '%s %s/%s [%s]: ' % (prompt, true_value, false_value, default_value) i = input(prompt) if not i: return default while True: if i == true_value: return True elif i == false_value: return False prompt = 'Please input %s or %s: ' % (true_value, false_value) i = input(prompt) def main(): ''' 主函数 ''' # op = yes_or_no('请确保手机打开了 ADB 并连接了电脑,然后打开跳一跳并【开始游戏】后再用本程序,确定开始?') # if not op: # print('bye') # return # 初始化AI ai.init() print('程序版本号:{}'.format(VERSION)) debug.dump_device_info() check_screenshot() i, next_rest, next_rest_time = 0, random.randrange(3, 10), random.randrange(5, 10) while True: pull_screenshot() im = Image.open('./autojump.png') # 获取棋子和 board 的位置 piece_x, piece_y, board_x, board_y = find_piece_and_board(im) ts = int(time.time()) # print(ts, piece_x, piece_y, board_x, board_y) set_button_position(im) press_time = jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2)) # 在跳跃落下的瞬间 摄像机移动前截图 这个参数要自己校调 time.sleep(0.2) pull_screenshot_temp() im_temp = Image.open('./autojump_temp.png') temp_piece_x, temp_piece_y = find_piece(im_temp) debug.computing_error(press_time, board_x, board_y, piece_x, piece_y, temp_piece_x, temp_piece_y) if debug_switch: debug.save_debug_screenshot(ts, im, piece_x, piece_y, board_x, board_y) debug.save_debug_screenshot(ts, im_temp, temp_piece_x, temp_piece_y, board_x, board_y) # debug.backup_screenshot(ts) i = 0 if i == next_rest: print('已经连续打了 {} 下,休息 {}s'.format(i, next_rest_time)) for j in range(next_rest_time): sys.stdout.write('\r程序将在 {}s 后继续'.format(next_rest_time - j)) sys.stdout.flush() time.sleep(1) print('\n继续') i, next_rest, next_rest_time = 0, random.randrange(30, 100), random.randrange(10, 60) time.sleep(random.uniform(0.5, 0.6)) # 为了保证截图的时候应落稳了,多延迟一会儿,随机值防 ban if __name__ == '__main__': try: main() except KeyboardInterrupt: adb.run('kill-server') print('bye') exit(0) ================================================ FILE: wechat_jump_auto_curves.py ================================================ # -*- coding: utf-8 -*- """ ##基于python3.5(64位) ###如果缺少scikit-image库,建议进下面网址下载whl直接安装 ##https://www.lfd.uci.edu/~gohlke/pythonlibs/#scikit-image === 思路 === 核心:每次落稳之后截图,根据截图算出棋子的坐标和下一个块顶面的中点坐标, 根据两个点的距离乘以一个时间系数获得长按的时间 识别棋子:靠棋子的颜色来识别位置,通过截图发现最下面一行大概是一条 直线,就从上往下一行一行遍历,比较颜色(颜色用了一个区间来比较) 找到最下面的那一行的所有点,然后求个中点,求好之后再让 Y 轴坐标 减小棋子底盘的一半高度从而得到中心点的坐标 识别棋盘:靠底色和方块的色差来做,从分数之下的位置开始,一行一行扫描, 由于圆形的块最顶上是一条线,方形的上面大概是一个点,所以就 用类似识别棋子的做法多识别了几个点求中点,这时候得到了块中点的 X 轴坐标,这时候假设现在棋子在当前块的中心,根据一个通过截图获取的 固定的角度来推出中点的 Y 坐标 最后:根据两点的坐标算距离乘以系数来获取长按时间(似乎可以直接用 X 轴距离) """ from __future__ import print_function, division import sys import time import math import random from PIL import Image from six.moves import input from skimage import io,transform import numpy as np import tensorflow as tf try: from common import debug, config, screenshot, UnicodeStreamFilter from common.auto_adb import auto_adb except Exception as ex: print(ex) print('请将脚本放在项目根目录中运行') print('请检查项目根目录中的 common 文件夹是否存在') exit(1) adb = auto_adb() VERSION = "1.1.4" # DEBUG 开关,需要调试的时候请改为 True,不需要调试的时候为 False DEBUG_SWITCH = False # Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需 # 设置,设置保存在 config 文件夹中 config = config.open_accordant_config() under_game_score_y = config['under_game_score_y'] # 长按的时间系数,请自己根据实际情况调节 press_coefficient = config['press_coefficient'] # 二分之一的棋子底座高度,可能要调节 piece_base_height_1_2 = config['piece_base_height_1_2'] # 棋子的宽度,比截图中量到的稍微大一点比较安全,可能要调节 piece_body_width = config['piece_body_width'] target_score=1024 ##目标分数 total_step=30 ##达到目标次数所需游戏次数 start_score=100 ##设置第一次分数(目前分数) def set_button_position(im): """ 将 swipe 设置为 `再来一局` 按钮的位置 """ global swipe_x1, swipe_y1, swipe_x2, swipe_y2 w, h = im.size left = int(w / 2) top = int(1584 * (h / 1920.0)) left = int(random.uniform(left - 100, left + 100)) top = int(random.uniform(top - 100, top + 100)) # 随机防 ban after_top = int(random.uniform(top - 100, top + 100)) after_left = int(random.uniform(left - 100, left + 100)) swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, after_left, after_top def jump(distance): """ 跳跃一定的距离 """ press_time = distance * press_coefficient press_time = max(press_time, 200) # 设置 200ms 是最小的按压时间 press_time = int(press_time) cmd = 'shell input swipe {x1} {y1} {x2} {y2} {duration}'.format( x1=swipe_x1, y1=swipe_y1, x2=swipe_x2, y2=swipe_y2, duration=press_time ) print('{} {}'.format(adb.adb_path, cmd)) adb.run(cmd) return press_time def find_piece_and_board(im): """ 寻找关键坐标 """ w, h = im.size piece_x_sum = 0 piece_x_c = 0 piece_y_max = 0 board_x = 0 board_y = 0 scan_x_border = int(w / 8) # 扫描棋子时的左右边界 scan_start_y = 0 # 扫描的起始 y 坐标 im_pixel = im.load() # 以 50px 步长,尝试探测 scan_start_y for i in range(int(h / 3), int(h*2 / 3), 50): last_pixel = im_pixel[0, i] for j in range(1, w): pixel = im_pixel[j, i] # 不是纯色的线,则记录 scan_start_y 的值,准备跳出循环 if pixel != last_pixel: scan_start_y = i - 50 break if scan_start_y: break print('scan_start_y: {}'.format(scan_start_y)) # 从 scan_start_y 开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过 2/3 for i in range(scan_start_y, int(h * 2 / 3)): # 横坐标方面也减少了一部分扫描开销 for j in range(scan_x_border, w - scan_x_border): pixel = im_pixel[j, i] # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜 # 色这样应该 OK,暂时不提出来 if (50 < pixel[0] < 60) \ and (53 < pixel[1] < 63) \ and (95 < pixel[2] < 110): piece_x_sum += j piece_x_c += 1 piece_y_max = max(i, piece_y_max) if not all((piece_x_sum, piece_x_c)): return 0, 0, 0, 0 piece_x = int(piece_x_sum / piece_x_c) piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 # 限制棋盘扫描的横坐标,避免音符 bug if piece_x < w/2: board_x_start = piece_x board_x_end = w else: board_x_start = 0 board_x_end = piece_x for i in range(int(h / 3), int(h * 2 / 3)): last_pixel = im_pixel[0, i] if board_x or board_y: break board_x_sum = 0 board_x_c = 0 for j in range(int(board_x_start), int(board_x_end)): pixel = im_pixel[j, i] # 修掉脑袋比下一个小格子还高的情况的 bug if abs(j - piece_x) < piece_body_width: continue # 修掉圆顶的时候一条线导致的小 bug,这个颜色判断应该 OK,暂时不提出来 if abs(pixel[0] - last_pixel[0]) \ + abs(pixel[1] - last_pixel[1]) \ + abs(pixel[2] - last_pixel[2]) > 10: board_x_sum += j board_x_c += 1 if board_x_sum: board_x = board_x_sum / board_x_c last_pixel = im_pixel[board_x, i] # 从上顶点往下 +274 的位置开始向上找颜色与上顶点一样的点,为下顶点 # 该方法对所有纯色平面和部分非纯色平面有效,对高尔夫草坪面、木纹桌面、 # 药瓶和非菱形的碟机(好像是)会判断错误 for k in range(i+274, i, -1): # 274 取开局时最大的方块的上下顶点距离 pixel = im_pixel[board_x, k] if abs(pixel[0] - last_pixel[0]) \ + abs(pixel[1] - last_pixel[1]) \ + abs(pixel[2] - last_pixel[2]) < 10: break board_y = int((i+k) / 2) # 如果上一跳命中中间,则下个目标中心会出现 r245 g245 b245 的点,利用这个 # 属性弥补上一段代码可能存在的判断错误 # 若上一跳由于某种原因没有跳到正中间,而下一跳恰好有无法正确识别花纹,则有 # 可能游戏失败,由于花纹面积通常比较大,失败概率较低 for j in range(i, i+200): pixel = im_pixel[board_x, j] if abs(pixel[0] - 245) + abs(pixel[1] - 245) + abs(pixel[2] - 245) == 0: board_y = j + 10 break if not all((board_x, board_y)): return 0, 0, 0, 0 return piece_x, piece_y, board_x, board_y def yes_or_no(prompt, true_value='y', false_value='n', default=True): """ 检查是否已经为启动程序做好了准备 """ default_value = true_value if default else false_value prompt = '{} {}/{} [{}]: '.format(prompt, true_value, false_value, default_value) i = input(prompt) if not i: return default while True: if i == true_value: return True elif i == false_value: return False prompt = 'Please input {} or {}: '.format(true_value, false_value) i = input(prompt) def pross_data(image): pixels = list(image.getdata()) # 得到像素数据 灰度0-255 #print(len(pixels)) for i in range(len(pixels)): if pixels[i]<100: pixels[i]=0 else: pixels[i]=255 return pixels def pixel_division(img,w,h): pixels = list(img.getdata()) row_pix=np.zeros([1,h]) col_pix=np.zeros([1,w]) for i in range(w): for j in range(h): if pixels[j*w+i]<100: row_pix[0,j]+=1 col_pix[0,i]+=1 start_h=0 end_h=0 flag=0 for j in range(h): if row_pix[0,j]>=1 and flag==0: start_h=j flag=1 if row_pix[0,j]>=1: end_h=j pixels_Widh=[] end_w=0 for i in range(1,w): if col_pix[0,i-1]<=0 and col_pix[0,i]>=1: pixels_Widh.append(i-1) if col_pix[0,i]>=1: end_w=i pixels_Widh.append(end_w+1) return start_h,end_h,pixels_Widh def strint(score0): if(score0<10): return str(score0) else: return "" def read_one_image(path): img = io.imread(path) w=81 h=81 c=1 img = transform.resize(img,(w,h,c)) return np.asarray(img) def main(): """ 主函数 """ op = yes_or_no('请确保手机打开了 ADB 并连接了电脑,' '然后打开跳一跳并【开始游戏】后再用本程序,确定开始?') if not op: print('bye') return print('程序版本号:{}'.format(VERSION)) debug.dump_device_info() screenshot.check_screenshot() i, next_rest, next_rest_time = (0, random.randrange(3, 10), random.randrange(5, 10)) j= 0 ################ 分数曲线公式 y_score=[] next_start=0 global start_score for i in range(total_step): each_score=target_score*(1-np.exp(-0.15*(1024.0/target_score)*i)) y_score.append(each_score) if start_score>each_score: next_start=i next_start+=1 #print(y_score) if start_score y_score[next_start]: ##自动结束这一次 print("----------------") jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2)*5) next_start+=1 time.sleep(5*random.random()) if next_start >len(y_score): break jump(math.sqrt((board_x - piece_x) ** 2 + (board_y - piece_y) ** 2)) if DEBUG_SWITCH: debug.save_debug_screenshot(ts, im, piece_x, piece_y, board_x, board_y) debug.backup_screenshot(ts) im.close() i += 1 j += 1 if i == next_rest: print('已经连续打了 {} 下,休息 {}s'.format(i, next_rest_time)) for j in range(next_rest_time): sys.stdout.write('\r程序将在 {}s 后继续'.format(next_rest_time - j)) sys.stdout.flush() time.sleep(1) print('\n继续') i, next_rest, next_rest_time = (0, random.randrange(30, 100), random.randrange(10, 60)) # 为了保证截图的时候应落稳了,多延迟一会儿,随机值防 ban time.sleep(random.uniform(0.9, 1.2)) if __name__ == '__main__': try: main() except KeyboardInterrupt: adb.run('kill-server') print('bye') exit(0) ================================================ FILE: wechat_jump_auto_iOS.py ================================================ # -*- coding: utf-8 -*- """ # === 思路 === # 核心:每次落稳之后截图,根据截图算出棋子的坐标和下一个块顶面的中点坐标, # 根据两个点的距离乘以一个时间系数获得长按的时间 # 识别棋子:靠棋子的颜色来识别位置,通过截图发现最下面一行大概是一条 直线,就从上往下一行一行遍历,比较颜色(颜色用了一个区间来比较) 找到最下面的那一行的所有点,然后求个中点,求好之后再让 Y 轴坐标 减小棋子底盘的一半高度从而得到中心点的坐标 # 识别棋盘:靠底色和方块的色差来做,从分数之下的位置开始,一行一行扫描, 由于圆形的块最顶上是一条线,方形的上面大概是一个点,所以就 用类似识别棋子的做法多识别了几个点求中点,这时候得到了块中点的 X 轴坐标,这时候假设现在棋子在当前块的中心,根据一个通过截图获取的 固定的角度来推出中点的 Y 坐标 # 最后:根据两点的坐标算距离乘以系数来获取长按时间(似乎可以直接用 X 轴距离) """ import os import shutil import time import math import random import json from PIL import Image, ImageDraw import wda with open('config.json', 'r') as f: config = json.load(f) # Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需设置 under_game_score_y = config['under_game_score_y'] # 长按的时间系数,请自己根据实际情况调节 press_coefficient = config['press_coefficient'] # 二分之一的棋子底座高度,可能要调节 piece_base_height_1_2 = config['piece_base_height_1_2'] # 棋子的宽度,比截图中量到的稍微大一点比较安全,可能要调节 piece_body_width = config['piece_body_width'] time_coefficient = config['press_coefficient'] # 模拟按压的起始点坐标,需要自动重复游戏请设置成“再来一局”的坐标 swipe = config.get('swipe', { "x1": 320, "y1": 410, "x2": 320, "y2": 410 }) VERSION = "1.1.4" c = wda.Client() s = c.session() screenshot_backup_dir = 'screenshot_backups/' if not os.path.isdir(screenshot_backup_dir): os.mkdir(screenshot_backup_dir) def pull_screenshot(): c.screenshot('1.png') def jump(distance): press_time = distance * time_coefficient / 1000 print('press time: {}'.format(press_time)) s.tap_hold(random.uniform(0, 320), random.uniform(64, 320), press_time) def backup_screenshot(ts): """ 为了方便失败的时候 debug """ if not os.path.isdir(screenshot_backup_dir): os.mkdir(screenshot_backup_dir) shutil.copy('1.png', '{}{}.png'.format(screenshot_backup_dir, ts)) def save_debug_creenshot(ts, im, piece_x, piece_y, board_x, board_y): draw = ImageDraw.Draw(im) # 对debug图片加上详细的注释 draw.line((piece_x, piece_y) + (board_x, board_y), fill=2, width=3) draw.line((piece_x, 0, piece_x, im.size[1]), fill=(255, 0, 0)) draw.line((0, piece_y, im.size[0], piece_y), fill=(255, 0, 0)) draw.line((board_x, 0, board_x, im.size[1]), fill=(0, 0, 255)) draw.line((0, board_y, im.size[0], board_y), fill=(0, 0, 255)) draw.ellipse( (piece_x - 10, piece_y - 10, piece_x + 10, piece_y + 10), fill=(255, 0, 0)) draw.ellipse( (board_x - 10, board_y - 10, board_x + 10, board_y + 10), fill=(0, 0, 255)) del draw im.save('{}{}_d.png'.format(screenshot_backup_dir, ts)) def set_button_position(im): """ 将swipe设置为 `再来一局` 按钮的位置 """ global swipe_x1, swipe_y1, swipe_x2, swipe_y2 w, h = im.size left = w / 2 top = 1003 * (h / 1280.0) + 10 swipe_x1, swipe_y1, swipe_x2, swipe_y2 = left, top, left, top def find_piece_and_board(im): w, h = im.size print("size: {}, {}".format(w, h)) piece_x_sum = piece_x_c = piece_y_max = 0 board_x = board_y = 0 scan_x_border = int(w / 8) # 扫描棋子时的左右边界 scan_start_y = 0 # 扫描的起始 y 坐标 im_pixel = im.load() # 以 50px 步长,尝试探测 scan_start_y for i in range(under_game_score_y, h, 50): last_pixel = im_pixel[0, i] for j in range(1, w): pixel = im_pixel[j, i] # 不是纯色的线,则记录scan_start_y的值,准备跳出循环 if pixel != last_pixel: scan_start_y = i - 50 break if scan_start_y: break print("scan_start_y: ", scan_start_y) # 从 scan_start_y 开始往下扫描,棋子应位于屏幕上半部分,这里暂定不超过 2/3 for i in range(scan_start_y, int(h * 2 / 3)): # 横坐标方面也减少了一部分扫描开销 for j in range(scan_x_border, w - scan_x_border): pixel = im_pixel[j, i] # 根据棋子的最低行的颜色判断,找最后一行那些点的平均值,这个颜 # 色这样应该 OK,暂时不提出来 if (50 < pixel[0] < 60) \ and (53 < pixel[1] < 63) \ and (95 < pixel[2] < 110): piece_x_sum += j piece_x_c += 1 piece_y_max = max(i, piece_y_max) if not all((piece_x_sum, piece_x_c)): return 0, 0, 0, 0 piece_x = piece_x_sum / piece_x_c piece_y = piece_y_max - piece_base_height_1_2 # 上移棋子底盘高度的一半 for i in range(int(h / 3), int(h * 2 / 3)): last_pixel = im_pixel[0, i] if board_x or board_y: break board_x_sum = 0 board_x_c = 0 for j in range(w): pixel = im_pixel[j, i] # 修掉脑袋比下一个小格子还高的情况的 bug if abs(j - piece_x) < piece_body_width: continue # 修掉圆顶的时候一条线导致的小 bug,这个颜色判断应该 OK,暂时不提出来 if abs(pixel[0] - last_pixel[0]) \ + abs(pixel[1] - last_pixel[1]) \ + abs(pixel[2] - last_pixel[2]) > 10: board_x_sum += j board_x_c += 1 if board_x_sum: board_x = board_x_sum / board_x_c # 按实际的角度来算,找到接近下一个 board 中心的坐标 这里的角度应该 # 是 30°,值应该是 tan 30°, math.sqrt(3) / 3 board_y = piece_y - abs(board_x - piece_x) * math.sqrt(3) / 3 if not all((board_x, board_y)): return 0, 0, 0, 0 return piece_x, piece_y, board_x, board_y def main(): while True: pull_screenshot() im = Image.open("./1.png") # 获取棋子和 board 的位置 piece_x, piece_y, board_x, board_y = find_piece_and_board(im) ts = int(time.time()) print(ts, piece_x, piece_y, board_x, board_y) if piece_x == 0: return set_button_position(im) distance = math.sqrt( (board_x - piece_x) ** 2 + (board_y - piece_y) ** 2) jump(distance) save_debug_creenshot(ts, im, piece_x, piece_y, board_x, board_y) backup_screenshot(ts) # 为了保证截图的时候应落稳了,多延迟一会儿,随机值防 ban time.sleep(random.uniform(1, 1.1)) if __name__ == '__main__': main() ================================================ FILE: wechat_jump_auto_slim.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- __author__ = 'Erimus' ''' 这个是精简版本,只取x轴距离。 可以适配任意屏幕。 把磁盘读写截图改为内存读写。 可以防止被ban(从抓包数据看没有返回Error)。 ''' import os import sys import subprocess import time import random from PIL import Image, ImageDraw from io import BytesIO # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ VERSION = "1.1.4" screenshot_way = 2 def check_screenshot(): # 检查获取截图的方式 global screenshot_way if (screenshot_way < 0): print('暂不支持当前设备') sys.exit() binary_screenshot = pull_screenshot() try: Image.open(BytesIO(binary_screenshot)).load() # 直接使用内存IO print('Capture Method: {}'.format(screenshot_way)) except Exception: screenshot_way -= 1 check_screenshot() def pull_screenshot(): # 获取截图 global screenshot_way if screenshot_way in [1, 2]: process = subprocess.Popen( 'adb shell screencap -p', shell=True, stdout=subprocess.PIPE) screenshot = process.stdout.read() if screenshot_way == 2: binary_screenshot = screenshot.replace(b'\r\n', b'\n') else: binary_screenshot = screenshot.replace(b'\r\r\n', b'\n') return binary_screenshot elif screenshot_way == 0: os.system('adb shell screencap -p /sdcard/autojump.png') os.system('adb pull /sdcard/autojump.png .') # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ def find_piece_and_board(im): # 寻找起点和终点坐标 w, h = im.size # 图片宽高 im_pixel = im.load() def find_piece(pixel): # 棋子取色精确范围 return ((40 < pixel[0] < 65) and (40 < pixel[1] < 65) and (80 < pixel[2] < 105)) # 寻找棋子 ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ # 粗查棋子位置 piece_found, piece_fx, piece_fy = 0, 0, 0 scan_piece_unit = w // 40 # 间隔单位 ny = (h + w) // 2 # 寻找下限 从画面中央的正方形的下缘开始 while ny > (h - w) // 2 and not piece_found: ny -= scan_piece_unit for nx in range(0, w, scan_piece_unit): pixel = im_pixel[nx, ny] if find_piece(pixel): piece_fx, piece_fy = nx, ny piece_found = True break print('%-12s %s,%s' % ('piece_fuzzy:', piece_fx, piece_fy)) if not piece_fx: return 0, 0 # 没找到棋子 # 精查棋子位置 piece_x, piece_x_set = 0, [] # 棋子x/棋子坐标集合 piece_width = w // 14 # 估算棋子宽度 piece_height = w // 5 # 估算棋子高度 for ny in range(piece_fy + scan_piece_unit, piece_fy - piece_height, -4): for nx in range(max(piece_fx - piece_width, 0), min(piece_fx + piece_width, w)): pixel = im_pixel[nx, ny] # print(nx,ny,pixel) if find_piece(pixel): piece_x_set.append(nx) if len(piece_x_set) > 10: piece_x = sum(piece_x_set) / len(piece_x_set) break print('%-12s %s' % ('p_exact_x:', piece_x)) # 寻找落点 ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ board_x = 0 # 限制棋盘扫描的横坐标 避免音符bug if piece_x < w / 2: board_x_start, board_x_end = w // 2, w # 起点和终点的中点是画面中心 else: board_x_start, board_x_end = 0, w // 2 # 寻找落点顶点 board_x_set = [] # 目标坐标集合/改为list避免去重 for by in range((h - w) // 2, (h + w) // 2, 4): bg_pixel = im_pixel[0, by] for bx in range(board_x_start, board_x_end): pixel = im_pixel[bx, by] # 修掉脑袋比下一个小格子还高的情况 屏蔽小人左右的范围 if abs(bx - piece_x) < piece_width: continue # 修掉圆顶的时候一条线导致的小bug 这个颜色判断应该OK if (abs(pixel[0] - bg_pixel[0]) + abs(pixel[1] - bg_pixel[1]) + abs(pixel[2] - bg_pixel[2]) > 10): board_x_set.append(bx) if len(board_x_set) > 10: board_x = sum(board_x_set) / len(board_x_set) print('%-12s %s' % ('target_x:', board_x)) break # 找到了退出 return piece_x, board_x # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ def set_button_position(im, gameover=0): # 重设点击位置 再来一局位置 w, h = im.size if h // 16 > w // 9 + 2: # 长窄屏 2px容差 获取ui描绘的高度 uih = int(w / 9 * 16) else: uih = h # uiw = int(uih / 16 * 9) # 如果游戏结束 点击再来一局 left = int(w / 2) # 按钮半宽约uiw//5 # 根据9:16实测按钮高度中心0.825 按钮半高约uiw//28 top = int((h - uih) / 2 + uih * 0.825) if gameover: return left, top # 游戏中点击 随机位置防 ban left = random.randint(w // 4, w - 20) # 避开左下角按钮 top = random.randint(h * 3 // 4, h - 20) return left, top # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ def jump(piece_x, board_x, im, swipe_x1, swipe_y1): distanceX = abs(board_x - piece_x) # 起点到目标的水平距离 shortEdge = min(im.size) # 屏幕宽度 jumpPercent = distanceX / shortEdge # 跳跃百分比 jumpFullWidth = 1700 # 跳过整个宽度 需要按压的毫秒数 press_time = round(jumpFullWidth * jumpPercent) # 按压时长 press_time = 0 if not press_time else max( press_time, 200) # press_time大于0时限定最小值 print('%-12s %.2f%% (%s/%s) | Press: %sms' % ('Distance:', jumpPercent * 100, distanceX, shortEdge, press_time)) cmd = 'adb shell input swipe {x1} {y1} {x2} {y2} {duration}'.format( x1=swipe_x1, y1=swipe_y1, x2=swipe_x1 + random.randint(-10, 10), # 模拟位移 y2=swipe_y1 + random.randint(-10, 10), duration=press_time ) os.system(cmd) # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ def main(): check_screenshot() # 检查截图 count = 0 while True: count += 1 print('---\n%-12s %s (%s)' % ('Times:', count, int(time.time()))) # 获取截图 binary_screenshot = pull_screenshot() im = Image.open(BytesIO(binary_screenshot)) w, h = im.size if w > h: im = im.rotate(-90, expand=True) # 添加图片方向判断 # print('image | w:%s | h:%s'%(w,h)) # 获取棋子和 board 的位置 piece_x, board_x = find_piece_and_board(im) gameover = 0 if all((piece_x, board_x)) else 1 swipe_x1, swipe_y1 = set_button_position( im, gameover=gameover) # 随机点击位置 # 标注截图并显示 # draw = ImageDraw.Draw(im) # draw.line([piece_x, 0, piece_x, h], fill='blue', width=1) # start # draw.line([board_x, 0, board_x, h], fill='red', width=1) # end # draw.ellipse([swipe_x1 - 16, swipe_y1 - 16, # swipe_x1 + 16, swipe_y1 + 16], fill='red') # click # im.show() jump(piece_x, board_x, im, swipe_x1, swipe_y1) wait = (random.random())**5 * 9 + 1 # 停1~9秒 指数越高平均间隔越短 print('---\nWait %.3f s...' % wait) time.sleep(wait) print('Continue!') # ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ if __name__ == '__main__': try: main() except KeyboardInterrupt: os.system('adb kill-server') print('bye') exit(0) ================================================ FILE: wechat_jump_iOS_py3.py ================================================ # -*- coding: utf-8 -*- import time import wda import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation from PIL import Image # 截图距离 * time_coefficient = 按键时长 # time_coefficient: # iphonex: 0.00125 # iphone6: 0.00196 # iphone6s plus: 0.00120 time_coefficient = 0.00120 VERSION = "1.1.4" c = wda.Client() s = c.session() def pull_screenshot(): c.screenshot('autojump.png') def jump(distance): press_time = distance * time_coefficient press_time = press_time print('press_time = ',press_time) s.tap_hold(200, 200, press_time) fig = plt.figure() pull_screenshot() img = np.array(Image.open('autojump.png')) im = plt.imshow(img, animated=True) update = True click_count = 0 cor = [] def update_data(): return np.array(Image.open('autojump.png')) def updatefig(*args): global update if update: time.sleep(1) pull_screenshot() im.set_array(update_data()) update = False return im, def on_click(event): global update global ix, iy global click_count global cor ix, iy = event.xdata, event.ydata coords = [(ix, iy)] print('now = ', coords) cor.append(coords) click_count += 1 if click_count > 1: click_count = 0 cor1 = cor.pop() cor2 = cor.pop() distance = (cor1[0][0] - cor2[0][0])**2 + (cor1[0][1] - cor2[0][1])**2 distance = distance ** 0.5 print('distance = ', distance) jump(distance) update = True fig.canvas.mpl_connect('button_press_event', on_click) ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True) plt.show() ================================================ FILE: wechat_jump_py3.py ================================================ # -*- coding: utf-8 -*- import os import time import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation from PIL import Image VERSION = "1.1.4" def pull_screenshot(): os.system('adb shell screencap -p /sdcard/autojump.png') os.system('adb pull /sdcard/autojump.png .') def jump(distance): press_time = distance * 1.35 press_time = int(press_time) cmd = 'adb shell input swipe 320 410 320 410 ' + str(press_time) print(cmd) os.system(cmd) fig = plt.figure() pull_screenshot() img = np.array(Image.open('autojump.png')) im = plt.imshow(img, animated=True) update = True click_count = 0 cor = [] def update_data(): return np.array(Image.open('autojump.png')) def updatefig(*args): global update if update: time.sleep(1.5) pull_screenshot() im.set_array(update_data()) update = False return im, def on_click(event): global update global ix, iy global click_count global cor ix, iy = event.xdata, event.ydata coords = [(ix, iy)] print('now = ', coords) cor.append(coords) click_count += 1 if click_count > 1: click_count = 0 cor1 = cor.pop() cor2 = cor.pop() distance = (cor1[0][0] - cor2[0][0])**2 + (cor1[0][1] - cor2[0][1])**2 distance = distance ** 0.5 print('distance = ', distance) jump(distance) update = True fig.canvas.mpl_connect('button_press_event', on_click) ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True) plt.show()