commit ce4887a4f5227a081e7ec7368ea2054428c83574 Author: LmeSzinc Date: Sun Mar 29 01:22:46 2020 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f93ffaab4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,229 @@ +### AzurLaneAutoScript +log/*.txt +log/error/*.png +config/*.ini +dev_tools/debug_tools/*.py +.idea + +# Created by .ignore support plugin (hsz.mobi) + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# 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/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/README.md b/README.md new file mode 100644 index 000000000..5a59b2927 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +AzurLaneAutoScript diff --git a/assets/campaign/C2.png b/assets/campaign/C2.png new file mode 100644 index 000000000..d5c6e1e4c Binary files /dev/null and b/assets/campaign/C2.png differ diff --git a/assets/campaign/CHAPTER_NEXT.png b/assets/campaign/CHAPTER_NEXT.png new file mode 100644 index 000000000..0e6098bf1 Binary files /dev/null and b/assets/campaign/CHAPTER_NEXT.png differ diff --git a/assets/campaign/CHAPTER_PREV.png b/assets/campaign/CHAPTER_PREV.png new file mode 100644 index 000000000..2939d8210 Binary files /dev/null and b/assets/campaign/CHAPTER_PREV.png differ diff --git a/assets/campaign/D3.png b/assets/campaign/D3.png new file mode 100644 index 000000000..a27ea20f7 Binary files /dev/null and b/assets/campaign/D3.png differ diff --git a/assets/campaign/EVENT_20200312CN_SP3.png b/assets/campaign/EVENT_20200312CN_SP3.png new file mode 100644 index 000000000..72a58501c Binary files /dev/null and b/assets/campaign/EVENT_20200312CN_SP3.png differ diff --git a/assets/campaign/MODE_CHANGE.BUTTON.png b/assets/campaign/MODE_CHANGE.BUTTON.png new file mode 100644 index 000000000..987878512 Binary files /dev/null and b/assets/campaign/MODE_CHANGE.BUTTON.png differ diff --git a/assets/campaign/MODE_CHANGE.png b/assets/campaign/MODE_CHANGE.png new file mode 100644 index 000000000..a64dca945 Binary files /dev/null and b/assets/campaign/MODE_CHANGE.png differ diff --git a/assets/campaign/OCR_OIL.png b/assets/campaign/OCR_OIL.png new file mode 100644 index 000000000..eb88b90d9 Binary files /dev/null and b/assets/campaign/OCR_OIL.png differ diff --git a/assets/campaign/TEMPLATE_STAGE_CLEAR.png b/assets/campaign/TEMPLATE_STAGE_CLEAR.png new file mode 100644 index 000000000..7ab990e6b Binary files /dev/null and b/assets/campaign/TEMPLATE_STAGE_CLEAR.png differ diff --git a/assets/combat/AUTOMATION_CONFIRM.png b/assets/combat/AUTOMATION_CONFIRM.png new file mode 100644 index 000000000..393f75ce0 Binary files /dev/null and b/assets/combat/AUTOMATION_CONFIRM.png differ diff --git a/assets/combat/AUTOMATION_CONFIRM_CHECK.png b/assets/combat/AUTOMATION_CONFIRM_CHECK.png new file mode 100644 index 000000000..a039d40a4 Binary files /dev/null and b/assets/combat/AUTOMATION_CONFIRM_CHECK.png differ diff --git a/assets/combat/AUTOMATION_OFF.png b/assets/combat/AUTOMATION_OFF.png new file mode 100644 index 000000000..e79f6a018 Binary files /dev/null and b/assets/combat/AUTOMATION_OFF.png differ diff --git a/assets/combat/AUTOMATION_ON.png b/assets/combat/AUTOMATION_ON.png new file mode 100644 index 000000000..4a254abb1 Binary files /dev/null and b/assets/combat/AUTOMATION_ON.png differ diff --git a/assets/combat/AUTOMATION_SWITCH.png b/assets/combat/AUTOMATION_SWITCH.png new file mode 100644 index 000000000..686d7e3a2 Binary files /dev/null and b/assets/combat/AUTOMATION_SWITCH.png differ diff --git a/assets/combat/BATTLE_PREPARATION.png b/assets/combat/BATTLE_PREPARATION.png new file mode 100644 index 000000000..50ebf88ae Binary files /dev/null and b/assets/combat/BATTLE_PREPARATION.png differ diff --git a/assets/combat/BATTLE_PREPARATION_WITH_OVERLAY.png b/assets/combat/BATTLE_PREPARATION_WITH_OVERLAY.png new file mode 100644 index 000000000..fc7399d76 Binary files /dev/null and b/assets/combat/BATTLE_PREPARATION_WITH_OVERLAY.png differ diff --git a/assets/combat/BATTLE_STATUS_A.BUTTON.png b/assets/combat/BATTLE_STATUS_A.BUTTON.png new file mode 100644 index 000000000..882e8a8f0 Binary files /dev/null and b/assets/combat/BATTLE_STATUS_A.BUTTON.png differ diff --git a/assets/combat/BATTLE_STATUS_A.png b/assets/combat/BATTLE_STATUS_A.png new file mode 100644 index 000000000..f4f6ad834 Binary files /dev/null and b/assets/combat/BATTLE_STATUS_A.png differ diff --git a/assets/combat/BATTLE_STATUS_S.BUTTON.png b/assets/combat/BATTLE_STATUS_S.BUTTON.png new file mode 100644 index 000000000..882e8a8f0 Binary files /dev/null and b/assets/combat/BATTLE_STATUS_S.BUTTON.png differ diff --git a/assets/combat/BATTLE_STATUS_S.png b/assets/combat/BATTLE_STATUS_S.png new file mode 100644 index 000000000..435e690a4 Binary files /dev/null and b/assets/combat/BATTLE_STATUS_S.png differ diff --git a/assets/combat/EXP_INFO_A.BUTTON.png b/assets/combat/EXP_INFO_A.BUTTON.png new file mode 100644 index 000000000..882e8a8f0 Binary files /dev/null and b/assets/combat/EXP_INFO_A.BUTTON.png differ diff --git a/assets/combat/EXP_INFO_A.png b/assets/combat/EXP_INFO_A.png new file mode 100644 index 000000000..d6a847475 Binary files /dev/null and b/assets/combat/EXP_INFO_A.png differ diff --git a/assets/combat/EXP_INFO_S.BUTTON.png b/assets/combat/EXP_INFO_S.BUTTON.png new file mode 100644 index 000000000..882e8a8f0 Binary files /dev/null and b/assets/combat/EXP_INFO_S.BUTTON.png differ diff --git a/assets/combat/EXP_INFO_S.png b/assets/combat/EXP_INFO_S.png new file mode 100644 index 000000000..f12e5027a Binary files /dev/null and b/assets/combat/EXP_INFO_S.png differ diff --git a/assets/combat/GET_ITEMS_1.BUTTON.png b/assets/combat/GET_ITEMS_1.BUTTON.png new file mode 100644 index 000000000..882e8a8f0 Binary files /dev/null and b/assets/combat/GET_ITEMS_1.BUTTON.png differ diff --git a/assets/combat/GET_ITEMS_1.png b/assets/combat/GET_ITEMS_1.png new file mode 100644 index 000000000..e2417e4f3 Binary files /dev/null and b/assets/combat/GET_ITEMS_1.png differ diff --git a/assets/combat/GET_ITEMS_2.BUTTON.png b/assets/combat/GET_ITEMS_2.BUTTON.png new file mode 100644 index 000000000..882e8a8f0 Binary files /dev/null and b/assets/combat/GET_ITEMS_2.BUTTON.png differ diff --git a/assets/combat/GET_ITEMS_2.png b/assets/combat/GET_ITEMS_2.png new file mode 100644 index 000000000..9d73290fb Binary files /dev/null and b/assets/combat/GET_ITEMS_2.png differ diff --git a/assets/combat/GET_MISSION.png b/assets/combat/GET_MISSION.png new file mode 100644 index 000000000..6c210825e Binary files /dev/null and b/assets/combat/GET_MISSION.png differ diff --git a/assets/combat/GET_SHIP.BUTTON.png b/assets/combat/GET_SHIP.BUTTON.png new file mode 100644 index 000000000..882e8a8f0 Binary files /dev/null and b/assets/combat/GET_SHIP.BUTTON.png differ diff --git a/assets/combat/GET_SHIP.png b/assets/combat/GET_SHIP.png new file mode 100644 index 000000000..84c6baa15 Binary files /dev/null and b/assets/combat/GET_SHIP.png differ diff --git a/assets/combat/LOADING_BAR.png b/assets/combat/LOADING_BAR.png new file mode 100644 index 000000000..a06fdb614 Binary files /dev/null and b/assets/combat/LOADING_BAR.png differ diff --git a/assets/combat/PAUSE.BUTTON.png b/assets/combat/PAUSE.BUTTON.png new file mode 100644 index 000000000..a76f5f2c3 Binary files /dev/null and b/assets/combat/PAUSE.BUTTON.png differ diff --git a/assets/combat/PAUSE.png b/assets/combat/PAUSE.png new file mode 100644 index 000000000..1cad0e513 Binary files /dev/null and b/assets/combat/PAUSE.png differ diff --git a/assets/daemon/AMBUSH_AVOID.BUTTON.png b/assets/daemon/AMBUSH_AVOID.BUTTON.png new file mode 100644 index 000000000..5f361911c Binary files /dev/null and b/assets/daemon/AMBUSH_AVOID.BUTTON.png differ diff --git a/assets/daemon/AMBUSH_AVOID.png b/assets/daemon/AMBUSH_AVOID.png new file mode 100644 index 000000000..8971c282f Binary files /dev/null and b/assets/daemon/AMBUSH_AVOID.png differ diff --git a/assets/daemon/AT_SEA.png b/assets/daemon/AT_SEA.png new file mode 100644 index 000000000..aa8367442 Binary files /dev/null and b/assets/daemon/AT_SEA.png differ diff --git a/assets/daemon/FLEET_PREPARATION.png b/assets/daemon/FLEET_PREPARATION.png new file mode 100644 index 000000000..a42f20255 Binary files /dev/null and b/assets/daemon/FLEET_PREPARATION.png differ diff --git a/assets/daemon/GET_EMERGENCY_REPAIR.png b/assets/daemon/GET_EMERGENCY_REPAIR.png new file mode 100644 index 000000000..edc09b080 Binary files /dev/null and b/assets/daemon/GET_EMERGENCY_REPAIR.png differ diff --git a/assets/daemon/GET_ITEMS.BUTTON.png b/assets/daemon/GET_ITEMS.BUTTON.png new file mode 100644 index 000000000..9f7e28716 Binary files /dev/null and b/assets/daemon/GET_ITEMS.BUTTON.png differ diff --git a/assets/daemon/GET_ITEMS.png b/assets/daemon/GET_ITEMS.png new file mode 100644 index 000000000..59e2c8c01 Binary files /dev/null and b/assets/daemon/GET_ITEMS.png differ diff --git a/assets/daemon/MAP_PREPARATION.png b/assets/daemon/MAP_PREPARATION.png new file mode 100644 index 000000000..d4c4ccd50 Binary files /dev/null and b/assets/daemon/MAP_PREPARATION.png differ diff --git a/assets/daemon/STORY_CHOOCE.png b/assets/daemon/STORY_CHOOCE.png new file mode 100644 index 000000000..2e652009c Binary files /dev/null and b/assets/daemon/STORY_CHOOCE.png differ diff --git a/assets/daemon/STORY_CHOOCE_2.png b/assets/daemon/STORY_CHOOCE_2.png new file mode 100644 index 000000000..f8c9b80c6 Binary files /dev/null and b/assets/daemon/STORY_CHOOCE_2.png differ diff --git a/assets/daemon/STORY_SKIP.BUTTON.png b/assets/daemon/STORY_SKIP.BUTTON.png new file mode 100644 index 000000000..c363d638f Binary files /dev/null and b/assets/daemon/STORY_SKIP.BUTTON.png differ diff --git a/assets/daemon/STORY_SKIP.png b/assets/daemon/STORY_SKIP.png new file mode 100644 index 000000000..17559a8c2 Binary files /dev/null and b/assets/daemon/STORY_SKIP.png differ diff --git a/assets/daemon/STORY_SKIP_CONFIRM.png b/assets/daemon/STORY_SKIP_CONFIRM.png new file mode 100644 index 000000000..976c8aa3e Binary files /dev/null and b/assets/daemon/STORY_SKIP_CONFIRM.png differ diff --git a/assets/daemon/STRATEGY_OPEN.BUTTON.png b/assets/daemon/STRATEGY_OPEN.BUTTON.png new file mode 100644 index 000000000..5fc85421c Binary files /dev/null and b/assets/daemon/STRATEGY_OPEN.BUTTON.png differ diff --git a/assets/daemon/STRATEGY_OPEN.png b/assets/daemon/STRATEGY_OPEN.png new file mode 100644 index 000000000..66a8bf721 Binary files /dev/null and b/assets/daemon/STRATEGY_OPEN.png differ diff --git a/assets/daily/DAILY_ACTIVE.png b/assets/daily/DAILY_ACTIVE.png new file mode 100644 index 000000000..4a3c4977d Binary files /dev/null and b/assets/daily/DAILY_ACTIVE.png differ diff --git a/assets/daily/DAILY_ENTER.png b/assets/daily/DAILY_ENTER.png new file mode 100644 index 000000000..3a03d0f52 Binary files /dev/null and b/assets/daily/DAILY_ENTER.png differ diff --git a/assets/daily/DAILY_ENTER_CHECK.png b/assets/daily/DAILY_ENTER_CHECK.png new file mode 100644 index 000000000..878154e7f Binary files /dev/null and b/assets/daily/DAILY_ENTER_CHECK.png differ diff --git a/assets/daily/DAILY_FLEET_NEXT.png b/assets/daily/DAILY_FLEET_NEXT.png new file mode 100644 index 000000000..e29006ed7 Binary files /dev/null and b/assets/daily/DAILY_FLEET_NEXT.png differ diff --git a/assets/daily/DAILY_FLEET_PREV.png b/assets/daily/DAILY_FLEET_PREV.png new file mode 100644 index 000000000..7b82a1202 Binary files /dev/null and b/assets/daily/DAILY_FLEET_PREV.png differ diff --git a/assets/daily/DAILY_MISSION_1.png b/assets/daily/DAILY_MISSION_1.png new file mode 100644 index 000000000..b7953dc92 Binary files /dev/null and b/assets/daily/DAILY_MISSION_1.png differ diff --git a/assets/daily/DAILY_MISSION_2.png b/assets/daily/DAILY_MISSION_2.png new file mode 100644 index 000000000..0a6c1ae45 Binary files /dev/null and b/assets/daily/DAILY_MISSION_2.png differ diff --git a/assets/daily/DAILY_MISSION_3.png b/assets/daily/DAILY_MISSION_3.png new file mode 100644 index 000000000..d56755a77 Binary files /dev/null and b/assets/daily/DAILY_MISSION_3.png differ diff --git a/assets/daily/DAILY_NEXT.png b/assets/daily/DAILY_NEXT.png new file mode 100644 index 000000000..93cc2a3f0 Binary files /dev/null and b/assets/daily/DAILY_NEXT.png differ diff --git a/assets/daily/DAILY_PREV.png b/assets/daily/DAILY_PREV.png new file mode 100644 index 000000000..c43fadc88 Binary files /dev/null and b/assets/daily/DAILY_PREV.png differ diff --git a/assets/daily/OCR_DAILY_FLEET_INDEX.png b/assets/daily/OCR_DAILY_FLEET_INDEX.png new file mode 100644 index 000000000..0ca1988eb Binary files /dev/null and b/assets/daily/OCR_DAILY_FLEET_INDEX.png differ diff --git a/assets/daily/OCR_REMAIN.png b/assets/daily/OCR_REMAIN.png new file mode 100644 index 000000000..7e602a20b Binary files /dev/null and b/assets/daily/OCR_REMAIN.png differ diff --git a/assets/equipment/EQUIPMENT_OPEN.png b/assets/equipment/EQUIPMENT_OPEN.png new file mode 100644 index 000000000..37bc2f569 Binary files /dev/null and b/assets/equipment/EQUIPMENT_OPEN.png differ diff --git a/assets/equipment/EQUIP_1.png b/assets/equipment/EQUIP_1.png new file mode 100644 index 000000000..eb33b6800 Binary files /dev/null and b/assets/equipment/EQUIP_1.png differ diff --git a/assets/equipment/EQUIP_2.png b/assets/equipment/EQUIP_2.png new file mode 100644 index 000000000..6c98373db Binary files /dev/null and b/assets/equipment/EQUIP_2.png differ diff --git a/assets/equipment/EQUIP_3.png b/assets/equipment/EQUIP_3.png new file mode 100644 index 000000000..c8fd42943 Binary files /dev/null and b/assets/equipment/EQUIP_3.png differ diff --git a/assets/equipment/EQUIP_OFF.png b/assets/equipment/EQUIP_OFF.png new file mode 100644 index 000000000..b2c6017e4 Binary files /dev/null and b/assets/equipment/EQUIP_OFF.png differ diff --git a/assets/equipment/EQUIP_OFF_CONFIRM.png b/assets/equipment/EQUIP_OFF_CONFIRM.png new file mode 100644 index 000000000..8a44a705e Binary files /dev/null and b/assets/equipment/EQUIP_OFF_CONFIRM.png differ diff --git a/assets/equipment/FLEET_ENTER.png b/assets/equipment/FLEET_ENTER.png new file mode 100644 index 000000000..04eb03415 Binary files /dev/null and b/assets/equipment/FLEET_ENTER.png differ diff --git a/assets/equipment/FLEET_NEXT.png b/assets/equipment/FLEET_NEXT.png new file mode 100644 index 000000000..19839f69f Binary files /dev/null and b/assets/equipment/FLEET_NEXT.png differ diff --git a/assets/equipment/FLEET_PREV.png b/assets/equipment/FLEET_PREV.png new file mode 100644 index 000000000..7d05eb729 Binary files /dev/null and b/assets/equipment/FLEET_PREV.png differ diff --git a/assets/equipment/OCR_FLEET_INDEX.png b/assets/equipment/OCR_FLEET_INDEX.png new file mode 100644 index 000000000..3ac28cfb2 Binary files /dev/null and b/assets/equipment/OCR_FLEET_INDEX.png differ diff --git a/assets/equipment/SWIPE_AREA.png b/assets/equipment/SWIPE_AREA.png new file mode 100644 index 000000000..bd1a0ef0f Binary files /dev/null and b/assets/equipment/SWIPE_AREA.png differ diff --git a/assets/exercise/EQUIP_EDIT_ACTIVE.png b/assets/exercise/EQUIP_EDIT_ACTIVE.png new file mode 100644 index 000000000..3cadc7b15 Binary files /dev/null and b/assets/exercise/EQUIP_EDIT_ACTIVE.png differ diff --git a/assets/exercise/EQUIP_EDIT_INACTIVE.png b/assets/exercise/EQUIP_EDIT_INACTIVE.png new file mode 100644 index 000000000..b94b1eb94 Binary files /dev/null and b/assets/exercise/EQUIP_EDIT_INACTIVE.png differ diff --git a/assets/exercise/EQUIP_ENTER.png b/assets/exercise/EQUIP_ENTER.png new file mode 100644 index 000000000..e341ff5fa Binary files /dev/null and b/assets/exercise/EQUIP_ENTER.png differ diff --git a/assets/exercise/EXERCISE_PREPARATION.png b/assets/exercise/EXERCISE_PREPARATION.png new file mode 100644 index 000000000..d96b723d3 Binary files /dev/null and b/assets/exercise/EXERCISE_PREPARATION.png differ diff --git a/assets/exercise/NEW_OPPONENT.png b/assets/exercise/NEW_OPPONENT.png new file mode 100644 index 000000000..41bceba78 Binary files /dev/null and b/assets/exercise/NEW_OPPONENT.png differ diff --git a/assets/exercise/OCR_EXERCISE_REMAIN.png b/assets/exercise/OCR_EXERCISE_REMAIN.png new file mode 100644 index 000000000..976c1502c Binary files /dev/null and b/assets/exercise/OCR_EXERCISE_REMAIN.png differ diff --git a/assets/exercise/OPPONENT_1.png b/assets/exercise/OPPONENT_1.png new file mode 100644 index 000000000..c4d3792ed Binary files /dev/null and b/assets/exercise/OPPONENT_1.png differ diff --git a/assets/exercise/OPPONENT_2.png b/assets/exercise/OPPONENT_2.png new file mode 100644 index 000000000..889e77ad7 Binary files /dev/null and b/assets/exercise/OPPONENT_2.png differ diff --git a/assets/exercise/OPPONENT_3.png b/assets/exercise/OPPONENT_3.png new file mode 100644 index 000000000..f1c858a9a Binary files /dev/null and b/assets/exercise/OPPONENT_3.png differ diff --git a/assets/exercise/OPPONENT_4.png b/assets/exercise/OPPONENT_4.png new file mode 100644 index 000000000..9fdb28edc Binary files /dev/null and b/assets/exercise/OPPONENT_4.png differ diff --git a/assets/exercise/QUIT_CONFIRM.png b/assets/exercise/QUIT_CONFIRM.png new file mode 100644 index 000000000..fb164581f Binary files /dev/null and b/assets/exercise/QUIT_CONFIRM.png differ diff --git a/assets/exercise/QUIT_RECONFIRM.png b/assets/exercise/QUIT_RECONFIRM.png new file mode 100644 index 000000000..8033f795d Binary files /dev/null and b/assets/exercise/QUIT_RECONFIRM.png differ diff --git a/assets/handler/GET_AMMO.png b/assets/handler/GET_AMMO.png new file mode 100644 index 000000000..4532cb32c Binary files /dev/null and b/assets/handler/GET_AMMO.png differ diff --git a/assets/handler/GET_ITEMS_1.BUTTON.png b/assets/handler/GET_ITEMS_1.BUTTON.png new file mode 100644 index 000000000..78e26346d Binary files /dev/null and b/assets/handler/GET_ITEMS_1.BUTTON.png differ diff --git a/assets/handler/GET_ITEMS_1.png b/assets/handler/GET_ITEMS_1.png new file mode 100644 index 000000000..e2417e4f3 Binary files /dev/null and b/assets/handler/GET_ITEMS_1.png differ diff --git a/assets/handler/GET_MISSION.png b/assets/handler/GET_MISSION.png new file mode 100644 index 000000000..54aa8a5c6 Binary files /dev/null and b/assets/handler/GET_MISSION.png differ diff --git a/assets/handler/INFO_BAR_1.png b/assets/handler/INFO_BAR_1.png new file mode 100644 index 000000000..9f1884161 Binary files /dev/null and b/assets/handler/INFO_BAR_1.png differ diff --git a/assets/handler/INFO_BAR_2.png b/assets/handler/INFO_BAR_2.png new file mode 100644 index 000000000..016291130 Binary files /dev/null and b/assets/handler/INFO_BAR_2.png differ diff --git a/assets/handler/INFO_BAR_3.png b/assets/handler/INFO_BAR_3.png new file mode 100644 index 000000000..6bb78cec1 Binary files /dev/null and b/assets/handler/INFO_BAR_3.png differ diff --git a/assets/handler/IN_MAP.png b/assets/handler/IN_MAP.png new file mode 100644 index 000000000..595590b2c Binary files /dev/null and b/assets/handler/IN_MAP.png differ diff --git a/assets/handler/IN_STAGE_BLUE.png b/assets/handler/IN_STAGE_BLUE.png new file mode 100644 index 000000000..c76f46b64 Binary files /dev/null and b/assets/handler/IN_STAGE_BLUE.png differ diff --git a/assets/handler/IN_STAGE_RED.png b/assets/handler/IN_STAGE_RED.png new file mode 100644 index 000000000..2af00db77 Binary files /dev/null and b/assets/handler/IN_STAGE_RED.png differ diff --git a/assets/handler/MAP_AIR_RAID.png b/assets/handler/MAP_AIR_RAID.png new file mode 100644 index 000000000..a1ffb384a Binary files /dev/null and b/assets/handler/MAP_AIR_RAID.png differ diff --git a/assets/handler/MAP_AMBUSH.png b/assets/handler/MAP_AMBUSH.png new file mode 100644 index 000000000..3f8ff115f Binary files /dev/null and b/assets/handler/MAP_AMBUSH.png differ diff --git a/assets/handler/MAP_AMBUSH_EVADE.BUTTON.png b/assets/handler/MAP_AMBUSH_EVADE.BUTTON.png new file mode 100644 index 000000000..d247de207 Binary files /dev/null and b/assets/handler/MAP_AMBUSH_EVADE.BUTTON.png differ diff --git a/assets/handler/MAP_AMBUSH_EVADE.png b/assets/handler/MAP_AMBUSH_EVADE.png new file mode 100644 index 000000000..25bda9864 Binary files /dev/null and b/assets/handler/MAP_AMBUSH_EVADE.png differ diff --git a/assets/handler/MAP_ENEMY_SEARCHING.png b/assets/handler/MAP_ENEMY_SEARCHING.png new file mode 100644 index 000000000..028728e24 Binary files /dev/null and b/assets/handler/MAP_ENEMY_SEARCHING.png differ diff --git a/assets/handler/MYSTERY_ITEM.png b/assets/handler/MYSTERY_ITEM.png new file mode 100644 index 000000000..6b18de5f8 Binary files /dev/null and b/assets/handler/MYSTERY_ITEM.png differ diff --git a/assets/handler/STRATEGY_OPEN_1.BUTTON.png b/assets/handler/STRATEGY_OPEN_1.BUTTON.png new file mode 100644 index 000000000..6ee139ff3 Binary files /dev/null and b/assets/handler/STRATEGY_OPEN_1.BUTTON.png differ diff --git a/assets/handler/STRATEGY_OPEN_1.png b/assets/handler/STRATEGY_OPEN_1.png new file mode 100644 index 000000000..efdd9d2ad Binary files /dev/null and b/assets/handler/STRATEGY_OPEN_1.png differ diff --git a/assets/handler/TEMPLATE_AMBUSH_EVADE_FAILED.png b/assets/handler/TEMPLATE_AMBUSH_EVADE_FAILED.png new file mode 100644 index 000000000..191721b09 Binary files /dev/null and b/assets/handler/TEMPLATE_AMBUSH_EVADE_FAILED.png differ diff --git a/assets/handler/TEMPLATE_AMBUSH_EVADE_SUCCESS.png b/assets/handler/TEMPLATE_AMBUSH_EVADE_SUCCESS.png new file mode 100644 index 000000000..68eabc4d3 Binary files /dev/null and b/assets/handler/TEMPLATE_AMBUSH_EVADE_SUCCESS.png differ diff --git a/assets/hard/EQUIP_ENTER_1.png b/assets/hard/EQUIP_ENTER_1.png new file mode 100644 index 000000000..55acc14f3 Binary files /dev/null and b/assets/hard/EQUIP_ENTER_1.png differ diff --git a/assets/hard/EQUIP_ENTER_2.png b/assets/hard/EQUIP_ENTER_2.png new file mode 100644 index 000000000..0c0959421 Binary files /dev/null and b/assets/hard/EQUIP_ENTER_2.png differ diff --git a/assets/hard/OCR_HARD_REMAIN.png b/assets/hard/OCR_HARD_REMAIN.png new file mode 100644 index 000000000..6de7f7cb8 Binary files /dev/null and b/assets/hard/OCR_HARD_REMAIN.png differ diff --git a/assets/map/FLEET_1_BAR.png b/assets/map/FLEET_1_BAR.png new file mode 100644 index 000000000..0efc322d9 Binary files /dev/null and b/assets/map/FLEET_1_BAR.png differ diff --git a/assets/map/FLEET_1_CHOOSE.png b/assets/map/FLEET_1_CHOOSE.png new file mode 100644 index 000000000..877c8d057 Binary files /dev/null and b/assets/map/FLEET_1_CHOOSE.png differ diff --git a/assets/map/FLEET_1_CLEAR.png b/assets/map/FLEET_1_CLEAR.png new file mode 100644 index 000000000..c4a695678 Binary files /dev/null and b/assets/map/FLEET_1_CLEAR.png differ diff --git a/assets/map/FLEET_1_IN_USE.png b/assets/map/FLEET_1_IN_USE.png new file mode 100644 index 000000000..145ac9e71 Binary files /dev/null and b/assets/map/FLEET_1_IN_USE.png differ diff --git a/assets/map/FLEET_2_BAR.png b/assets/map/FLEET_2_BAR.png new file mode 100644 index 000000000..4e8dfa623 Binary files /dev/null and b/assets/map/FLEET_2_BAR.png differ diff --git a/assets/map/FLEET_2_CHOOSE.png b/assets/map/FLEET_2_CHOOSE.png new file mode 100644 index 000000000..4e5a90c83 Binary files /dev/null and b/assets/map/FLEET_2_CHOOSE.png differ diff --git a/assets/map/FLEET_2_CLEAR.png b/assets/map/FLEET_2_CLEAR.png new file mode 100644 index 000000000..8c89a658e Binary files /dev/null and b/assets/map/FLEET_2_CLEAR.png differ diff --git a/assets/map/FLEET_2_IN_USE.png b/assets/map/FLEET_2_IN_USE.png new file mode 100644 index 000000000..f4f842610 Binary files /dev/null and b/assets/map/FLEET_2_IN_USE.png differ diff --git a/assets/map/FLEET_LOCKED.BUTTON.png b/assets/map/FLEET_LOCKED.BUTTON.png new file mode 100644 index 000000000..dffca7348 Binary files /dev/null and b/assets/map/FLEET_LOCKED.BUTTON.png differ diff --git a/assets/map/FLEET_LOCKED.png b/assets/map/FLEET_LOCKED.png new file mode 100644 index 000000000..6c3bf616a Binary files /dev/null and b/assets/map/FLEET_LOCKED.png differ diff --git a/assets/map/FLEET_PREPARATION.png b/assets/map/FLEET_PREPARATION.png new file mode 100644 index 000000000..a42f20255 Binary files /dev/null and b/assets/map/FLEET_PREPARATION.png differ diff --git a/assets/map/FLEET_PREPARATION_HARD_1.png b/assets/map/FLEET_PREPARATION_HARD_1.png new file mode 100644 index 000000000..0c580f319 Binary files /dev/null and b/assets/map/FLEET_PREPARATION_HARD_1.png differ diff --git a/assets/map/FLEET_PREPARATION_HARD_2.png b/assets/map/FLEET_PREPARATION_HARD_2.png new file mode 100644 index 000000000..324238232 Binary files /dev/null and b/assets/map/FLEET_PREPARATION_HARD_2.png differ diff --git a/assets/map/FLEET_UNLOCKED.BUTTON.png b/assets/map/FLEET_UNLOCKED.BUTTON.png new file mode 100644 index 000000000..dffca7348 Binary files /dev/null and b/assets/map/FLEET_UNLOCKED.BUTTON.png differ diff --git a/assets/map/FLEET_UNLOCKED.png b/assets/map/FLEET_UNLOCKED.png new file mode 100644 index 000000000..8e3bf3728 Binary files /dev/null and b/assets/map/FLEET_UNLOCKED.png differ diff --git a/assets/map/MAP_CAT_ATTACK.BUTTON.png b/assets/map/MAP_CAT_ATTACK.BUTTON.png new file mode 100644 index 000000000..e1eb7c9cf Binary files /dev/null and b/assets/map/MAP_CAT_ATTACK.BUTTON.png differ diff --git a/assets/map/MAP_CAT_ATTACK.png b/assets/map/MAP_CAT_ATTACK.png new file mode 100644 index 000000000..f382d05f7 Binary files /dev/null and b/assets/map/MAP_CAT_ATTACK.png differ diff --git a/assets/map/MAP_OFFENSIVE.png b/assets/map/MAP_OFFENSIVE.png new file mode 100644 index 000000000..e1eb7c9cf Binary files /dev/null and b/assets/map/MAP_OFFENSIVE.png differ diff --git a/assets/map/MAP_PREPARATION.png b/assets/map/MAP_PREPARATION.png new file mode 100644 index 000000000..8886ff706 Binary files /dev/null and b/assets/map/MAP_PREPARATION.png differ diff --git a/assets/map/SUBMARINE_BAR.png b/assets/map/SUBMARINE_BAR.png new file mode 100644 index 000000000..23c52bcda Binary files /dev/null and b/assets/map/SUBMARINE_BAR.png differ diff --git a/assets/map/SUBMARINE_CHOOSE.png b/assets/map/SUBMARINE_CHOOSE.png new file mode 100644 index 000000000..2b2a4ef37 Binary files /dev/null and b/assets/map/SUBMARINE_CHOOSE.png differ diff --git a/assets/map/SUBMARINE_CLEAR.png b/assets/map/SUBMARINE_CLEAR.png new file mode 100644 index 000000000..a9d476da7 Binary files /dev/null and b/assets/map/SUBMARINE_CLEAR.png differ diff --git a/assets/map/SUBMARINE_IN_USE.png b/assets/map/SUBMARINE_IN_USE.png new file mode 100644 index 000000000..55da34170 Binary files /dev/null and b/assets/map/SUBMARINE_IN_USE.png differ diff --git a/assets/map/SWITCH_OVER.png b/assets/map/SWITCH_OVER.png new file mode 100644 index 000000000..e241cf716 Binary files /dev/null and b/assets/map/SWITCH_OVER.png differ diff --git a/assets/map/WITHDRAW.png b/assets/map/WITHDRAW.png new file mode 100644 index 000000000..595590b2c Binary files /dev/null and b/assets/map/WITHDRAW.png differ diff --git a/assets/map/WITHDRAW_CONFIRM.png b/assets/map/WITHDRAW_CONFIRM.png new file mode 100644 index 000000000..b1de4b682 Binary files /dev/null and b/assets/map/WITHDRAW_CONFIRM.png differ diff --git a/assets/retire/EQUIP_CONFIRM.png b/assets/retire/EQUIP_CONFIRM.png new file mode 100644 index 000000000..0213a4b83 Binary files /dev/null and b/assets/retire/EQUIP_CONFIRM.png differ diff --git a/assets/retire/EQUIP_CONFIRM_2.png b/assets/retire/EQUIP_CONFIRM_2.png new file mode 100644 index 000000000..eaf0db878 Binary files /dev/null and b/assets/retire/EQUIP_CONFIRM_2.png differ diff --git a/assets/retire/GET_ITEMS_1_RETIREMENT_SAVE.png b/assets/retire/GET_ITEMS_1_RETIREMENT_SAVE.png new file mode 100644 index 000000000..2bfdd8559 Binary files /dev/null and b/assets/retire/GET_ITEMS_1_RETIREMENT_SAVE.png differ diff --git a/assets/retire/IN_RETIREMENT_CHECK.png b/assets/retire/IN_RETIREMENT_CHECK.png new file mode 100644 index 000000000..e43ccc50d Binary files /dev/null and b/assets/retire/IN_RETIREMENT_CHECK.png differ diff --git a/assets/retire/RETIRE_APPEAR_1.png b/assets/retire/RETIRE_APPEAR_1.png new file mode 100644 index 000000000..549f778a7 Binary files /dev/null and b/assets/retire/RETIRE_APPEAR_1.png differ diff --git a/assets/retire/RETIRE_APPEAR_2.png b/assets/retire/RETIRE_APPEAR_2.png new file mode 100644 index 000000000..9f1f72aad Binary files /dev/null and b/assets/retire/RETIRE_APPEAR_2.png differ diff --git a/assets/retire/RETIRE_APPEAR_3.png b/assets/retire/RETIRE_APPEAR_3.png new file mode 100644 index 000000000..4a6e40cc5 Binary files /dev/null and b/assets/retire/RETIRE_APPEAR_3.png differ diff --git a/assets/retire/SHIP_CONFIRM.png b/assets/retire/SHIP_CONFIRM.png new file mode 100644 index 000000000..7fb085e4c Binary files /dev/null and b/assets/retire/SHIP_CONFIRM.png differ diff --git a/assets/retire/SHIP_CONFIRM_2.png b/assets/retire/SHIP_CONFIRM_2.png new file mode 100644 index 000000000..92b2139db Binary files /dev/null and b/assets/retire/SHIP_CONFIRM_2.png differ diff --git a/assets/retire/SORTNG_CLICK.png b/assets/retire/SORTNG_CLICK.png new file mode 100644 index 000000000..73124c6c2 Binary files /dev/null and b/assets/retire/SORTNG_CLICK.png differ diff --git a/assets/retire/SORT_ASC.png b/assets/retire/SORT_ASC.png new file mode 100644 index 000000000..f6e018e96 Binary files /dev/null and b/assets/retire/SORT_ASC.png differ diff --git a/assets/retire/SORT_DESC.png b/assets/retire/SORT_DESC.png new file mode 100644 index 000000000..4cc656b81 Binary files /dev/null and b/assets/retire/SORT_DESC.png differ diff --git a/assets/ui/BACK_ARROW.BUTTON.png b/assets/ui/BACK_ARROW.BUTTON.png new file mode 100644 index 000000000..33878384b Binary files /dev/null and b/assets/ui/BACK_ARROW.BUTTON.png differ diff --git a/assets/ui/BACK_ARROW.png b/assets/ui/BACK_ARROW.png new file mode 100644 index 000000000..843cce04f Binary files /dev/null and b/assets/ui/BACK_ARROW.png differ diff --git a/assets/ui/CAMPAIGN_GOTO_DAILY.png b/assets/ui/CAMPAIGN_GOTO_DAILY.png new file mode 100644 index 000000000..67095ee1a Binary files /dev/null and b/assets/ui/CAMPAIGN_GOTO_DAILY.png differ diff --git a/assets/ui/CAMPAIGN_GOTO_EVENT.BUTTON.png b/assets/ui/CAMPAIGN_GOTO_EVENT.BUTTON.png new file mode 100644 index 000000000..371002b70 Binary files /dev/null and b/assets/ui/CAMPAIGN_GOTO_EVENT.BUTTON.png differ diff --git a/assets/ui/CAMPAIGN_GOTO_EVENT.png b/assets/ui/CAMPAIGN_GOTO_EVENT.png new file mode 100644 index 000000000..67095ee1a Binary files /dev/null and b/assets/ui/CAMPAIGN_GOTO_EVENT.png differ diff --git a/assets/ui/CAMPAIGN_GOTO_EXERCISE.png b/assets/ui/CAMPAIGN_GOTO_EXERCISE.png new file mode 100644 index 000000000..587b68bb3 Binary files /dev/null and b/assets/ui/CAMPAIGN_GOTO_EXERCISE.png differ diff --git a/assets/ui/DAILY_CHECK.png b/assets/ui/DAILY_CHECK.png new file mode 100644 index 000000000..4fd00a83e Binary files /dev/null and b/assets/ui/DAILY_CHECK.png differ diff --git a/assets/ui/EVENT_CHECK.png b/assets/ui/EVENT_CHECK.png new file mode 100644 index 000000000..14e3808a4 Binary files /dev/null and b/assets/ui/EVENT_CHECK.png differ diff --git a/assets/ui/EXERCISE_CHECK.png b/assets/ui/EXERCISE_CHECK.png new file mode 100644 index 000000000..41bceba78 Binary files /dev/null and b/assets/ui/EXERCISE_CHECK.png differ diff --git a/assets/ui/FLEET_CHECK.png b/assets/ui/FLEET_CHECK.png new file mode 100644 index 000000000..7aaf14f43 Binary files /dev/null and b/assets/ui/FLEET_CHECK.png differ diff --git a/assets/ui/GOTO_MAIN.BUTTON.png b/assets/ui/GOTO_MAIN.BUTTON.png new file mode 100644 index 000000000..42cde372d Binary files /dev/null and b/assets/ui/GOTO_MAIN.BUTTON.png differ diff --git a/assets/ui/GOTO_MAIN.png b/assets/ui/GOTO_MAIN.png new file mode 100644 index 000000000..527727928 Binary files /dev/null and b/assets/ui/GOTO_MAIN.png differ diff --git a/assets/ui/MAIN_GOTO_CAMPAIGN.png b/assets/ui/MAIN_GOTO_CAMPAIGN.png new file mode 100644 index 000000000..c81b8a13d Binary files /dev/null and b/assets/ui/MAIN_GOTO_CAMPAIGN.png differ diff --git a/assets/ui/MAIN_GOTO_FLEET.png b/assets/ui/MAIN_GOTO_FLEET.png new file mode 100644 index 000000000..5600b8703 Binary files /dev/null and b/assets/ui/MAIN_GOTO_FLEET.png differ diff --git a/assets/ui/OCR_OIL_CV.png b/assets/ui/OCR_OIL_CV.png new file mode 100644 index 000000000..b86949200 Binary files /dev/null and b/assets/ui/OCR_OIL_CV.png differ diff --git a/assets/ui/SP_CHECK.png b/assets/ui/SP_CHECK.png new file mode 100644 index 000000000..3fb7b8b22 Binary files /dev/null and b/assets/ui/SP_CHECK.png differ diff --git a/campaign/campaign_hard/campaign_hard.py b/campaign/campaign_hard/campaign_hard.py new file mode 100644 index 000000000..3b7f5cb71 --- /dev/null +++ b/campaign/campaign_hard/campaign_hard.py @@ -0,0 +1,92 @@ +from module.base.timer import Timer +from module.campaign.campaign_base import CampaignBase +from module.hard.equipment import HardEquipment +from module.logger import logger +from module.map.assets import MAP_PREPARATION, FLEET_PREPARATION +from module.map.exception import CampaignEnd +from module.ui.ui import CAMPAIGN_CHECK + + +class Config: + MAP_HAS_AMBUSH = False + EMOTION_REDUCE = False + +class Campaign(CampaignBase, HardEquipment): + def run(self): + logger.hr(self.ENTRANCE, level=2) + self.enter_map(self.ENTRANCE, mode='hard') + self.map = self.MAP + self.map.reset() + + if self.config.FLEET_HARD == 1: + self.ensure_edge_insight(reverse=True) + self.full_scan_find_boss() + else: + self.fleet_switch_click() + self.ensure_no_info_bar() + self.ensure_edge_insight() + self.full_scan_find_boss() + + try: + self.clear_boss() + except CampaignEnd: + logger.hr('Campaign end') + + def fleet_preparation(self): + self.equipment_take_on() + + @property + def _expected_combat_end(self): + return 'in_stage' + + def clear_boss(self): + grids = self.map.select(is_boss=True) + grids = grids.add(self.map.select(may_boss=True, is_enemy=True)) + logger.info('May boss: %s' % self.map.select(may_boss=True)) + logger.info('May boss and is enemy: %s' % self.map.select(may_boss=True, is_enemy=True)) + logger.info('Is boss: %s' % self.map.select(is_boss=True)) + # logger.info('Grids: %s' % grids) + if grids: + logger.hr('Clear BOSS') + grids = grids.sort(cost=True, weight=True) + logger.info('Grids: %s' % str(grids)) + self._goto(grids[0]) + raise CampaignEnd('BOSS Clear.') + + return False + + def equipment_take_off_when_finished(self): + logger.info('equipment_take_off_when_finished') + campaign_timer = Timer(2) + map_timer = Timer(1) + fleet_timer = Timer(1) + + while 1: + self.device.screenshot() + + # Enter campaign + if campaign_timer.reached() and self.appear_then_click(self.ENTRANCE): + campaign_timer.reset() + continue + + # Map preparation + if map_timer.reached() and self.appear(MAP_PREPARATION): + self.device.click(MAP_PREPARATION) + map_timer.reset() + campaign_timer.reset() + continue + + # Fleet preparation + if fleet_timer.reached() and self.appear(FLEET_PREPARATION): + self.equipment_take_off() + self.ui_back(check_button=CAMPAIGN_CHECK, appear_button=FLEET_PREPARATION) + break + + # Retire + if self.handle_retirement(): + continue + + # Emotion + pass + + return True diff --git a/campaign/campaign_main/campaign_10_4.py b/campaign/campaign_main/campaign_10_4.py new file mode 100644 index 000000000..944d4eb4f --- /dev/null +++ b/campaign/campaign_main/campaign_10_4.py @@ -0,0 +1,49 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + + +MAP = CampaignMap('10-4') +MAP.shape = 'I6' +MAP.map_data = ''' + MB ME ME -- ++ MB -- ++ ++ + ME ME ++ ME ++ -- ME -- ++ + -- -- ME ME ME ++ ME ME -- + ++ ++ ++ -- ME -- ME ++ ME + SP -- -- ME -- ++ -- ME -- + SP -- -- -- ME ++ ME -- MB +''' +MAP.weight_data = ''' + 10 10 10 10 10 10 10 10 10 + 10 20 10 10 10 10 10 10 10 + 10 10 10 10 30 10 10 20 10 + 10 10 10 10 10 10 10 10 20 + 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 +''' +# MAP.camera_data = ['D3'] +# MAP.spawn_data = [ +# {'battle': 0, 'enemy': 3}, +# {'battle': 1, 'enemy': 2, 'mystery': 1}, +# {'battle': 2, 'enemy': 2, 'mystery': 1}, +# {'battle': 3, 'enemy': 1, 'mystery': 2}, +# {'battle': 4, 'enemy': 1}, +# {'battle': 5, 'boss': 1}, +# ] + +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ + = MAP.flatten() + + +class Config: + pass + + +class Campaign(CampaignBase): + MAP = MAP diff --git a/campaign/campaign_main/campaign_12_4_leveling.py b/campaign/campaign_main/campaign_12_4_leveling.py new file mode 100644 index 000000000..1b3515148 --- /dev/null +++ b/campaign/campaign_main/campaign_12_4_leveling.py @@ -0,0 +1,99 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.exception import CampaignEnd +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + + +MAP = CampaignMap('12-4') +MAP.shape = 'K8' +MAP.map_data = ''' + MB ME ME -- ME ++ ++ ++ MB MB ++ + ME ++ -- ME -- MA ++ ++ ME ME ++ + -- ME -- ME ME -- ME ME -- ME -- + ++ -- ME ++ ++ ME ME -- ++ ++ ME + ++ ME ME -- ME ME -- ME -- ++ -- + ++ -- ME ME -- ME ME ++ -- -- ME + ME -- ME -- ME -- ME -- -- ME -- + -- -- -- ME SP SP ++ ++ ++ ME -- +''' +MAP.weight_data = ''' + 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 20 + 10 10 10 10 10 10 10 10 10 10 20 + 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 10 +''' +MAP.camera_data = ['D3', 'D6', 'H3', 'H6'] +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2}, + {'battle': 1, 'enemy': 2}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5}, + {'battle': 6, 'boss': 1}, +] +MAP.in_map_swipe_preset_data = (2, 1) + +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, K5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, K6, \ +A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, K7, \ +A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, K8, \ + = MAP.flatten() + +# ROAD_MAIN = RoadGrids([[B6, C5]]) + + +class Config: + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (120, 255 - 40), + 'width': 2, + 'prominence': 10, + 'distance': 35, + } + + +class Campaign(CampaignBase): + MAP = MAP + s3_enemy_count = 0 + non_s3_enemy_count = 0 + + + def check_s3_enemy(self): + if self.battle_count == 0: + self.s3_enemy_count = 0 + self.non_s3_enemy_count = 0 + + current = self.map.select(is_enemy=True, enemy_scale=3).count + logger.attr('S3_enemy', current) + + if self.battle_count == self.config.C124_NON_S3_ENTER_TOLERANCE: + if self.s3_enemy_count + current == 0: + self.withdraw() + elif self.battle_count > self.config.C124_NON_S3_ENTER_TOLERANCE: + if self.non_s3_enemy_count >= self.config.C124_NON_S3_WITHDRAW_TOLERANCE and current == 0: + self.withdraw() + + def battle_0(self): + self.check_s3_enemy() + + if self.battle_count >= self.config.C124_AMMO_PICK_UP: + self.pick_up_ammo() + + if self.clear_enemy(scale=(3,)): + self.s3_enemy_count += 1 + self.non_s3_enemy_count = 0 + return True + if self.clear_enemy(scale=(2,)): + self.non_s3_enemy_count += 1 + return True + + return self.battle_default() diff --git a/campaign/campaign_main/campaign_7_2.py b/campaign/campaign_main/campaign_7_2.py new file mode 100644 index 000000000..2d581f75a --- /dev/null +++ b/campaign/campaign_main/campaign_7_2.py @@ -0,0 +1,113 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + + +MAP = CampaignMap('7-2') +MAP.shape = 'H5' +MAP.map_data = ''' + ME ++ ME -- ME ME -- SP + MM ++ ++ MM -- -- ME -- + ME -- ME MB ME -- ME MM + -- ME -- MM -- ME ++ ++ + SP -- ME ME -- ME ++ ++ +''' +MAP.weight_data = ''' + 40 30 30 30 30 30 30 30 + 20 20 20 20 20 20 20 20 + 10 10 10 10 10 10 10 10 + 20 20 20 20 20 20 20 20 + 30 30 30 30 30 30 30 30 +''' +MAP.camera_data = ['D3'] +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3}, + {'battle': 1, 'enemy': 2, 'mystery': 1}, + {'battle': 2, 'enemy': 2, 'mystery': 1}, + {'battle': 3, 'enemy': 1, 'mystery': 2}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'boss': 1}, +] + +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5 = MAP.flatten() + +ROAD_MAIN = RoadGrids([A3, [C3, B4, C5], [F1, G2, G3]]) +GRIDS_FOR_FASTER = SelectedGrids([A3, C3, E3, G3]) +FLEET_2_STEP_ON = SelectedGrids([A3, G3, C3, E3]) + + +class Config: + pass + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + if self.fleet_2_step_on(FLEET_2_STEP_ON, roadblocks=[ROAD_MAIN]): + return True + + ignore = None + if self.fleet_at(A3, fleet=2): + ignore = SelectedGrids([A2]) + if self.fleet_at(G3, fleet=2): + ignore = SelectedGrids([H3]) + self.clear_all_mystery(nearby=True, ignore=ignore) + + if self.clear_roadblocks([ROAD_MAIN]): + return True + + if self.clear_enemy(scale=(3,)): + return True + if self.clear_potential_roadblocks([ROAD_MAIN]): + return True + if self.clear_grids_for_faster(GRIDS_FOR_FASTER): + return True + + return self.battle_default() + + battle_1 = battle_0 + battle_2 = battle_0 + + def battle_3(self): + if self.fleet_at(A3, fleet=2) and A2.is_mystery: + self.fleet_2.clear_chosen_mystery(A2) + self.fleet_2.goto(A3) + self.fleet_1.switch_to() + + if self.fleet_at(G3, fleet=2) and H3.is_mystery: + self.fleet_2.clear_chosen_mystery(H3) + self.fleet_2.goto(G3) + self.fleet_1.switch_to() + + return self.battle_0() + + def battle_4(self): + self.clear_all_mystery(nearby=False) + + if self.clear_roadblocks([ROAD_MAIN]): + return True + + if self.clear_enemy(scale=(3,)): + return True + # if self.clear_potential_roadblocks([ROAD_MAIN]): + # return True + if self.clear_grids_for_faster(GRIDS_FOR_FASTER): + return True + + return self.battle_default() + + def battle_5(self): + self.clear_all_mystery(nearby=False) + + if self.clear_roadblocks([ROAD_MAIN]): + return True + + return self.fleet_2.clear_boss() + # self.fleet_2.goto(D3) + # raise CampaignEnd('Boss cleared.') diff --git a/campaign/campaign_main/campaign_7_2_mystery_farming.py b/campaign/campaign_main/campaign_7_2_mystery_farming.py new file mode 100644 index 000000000..7d53e5bca --- /dev/null +++ b/campaign/campaign_main/campaign_7_2_mystery_farming.py @@ -0,0 +1,115 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + + +MAP = CampaignMap('7-2') +MAP.shape = 'H5' +MAP.map_data = ''' + ME ++ ME -- ME ME -- SP + MM ++ ++ MM -- -- ME -- + ME -- ME MB ME -- ME MM + -- ME -- MM -- ME ++ ++ + SP -- ME ME -- ME ++ ++ +''' +MAP.weight_data = ''' + 40 30 30 30 30 30 30 30 + 20 20 20 20 20 20 20 20 + 10 10 10 10 10 10 10 10 + 20 20 20 20 20 20 20 20 + 30 30 30 30 30 30 30 30 +''' +MAP.camera_data = ['D3'] +MAP.spawn_data = [ + {'battle': 0, 'enemy': 3}, + {'battle': 1, 'enemy': 2, 'mystery': 1}, + {'battle': 2, 'enemy': 2, 'mystery': 1}, + {'battle': 3, 'enemy': 1, 'mystery': 2}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'boss': 1}, +] +MAP.in_map_swipe_preset_data = (-1, 0) + +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5 = MAP.flatten() + +ROAD_MAIN = RoadGrids([A3, [C3, B4, C5], [F1, G2, G3]]) +GRIDS_FOR_FASTER = SelectedGrids([A3, C3, E3, G3]) +FLEET_2_STEP_ON = SelectedGrids([A3, G3, C3, E3]) + + +class Config: + # FLEET_2 = 0 + pass + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + if self.fleet_2_step_on(FLEET_2_STEP_ON, roadblocks=[ROAD_MAIN]): + return True + + ignore = None + if self.fleet_at(A3, fleet=2): + ignore = SelectedGrids([A2]) + if self.fleet_at(G3, fleet=2): + ignore = SelectedGrids([H3]) + self.clear_all_mystery(nearby=False, ignore=ignore) + + grids = ROAD_MAIN.roadblocks().select(is_accessible=True, enemy_scale=3) + if grids: + self.clear_chosen_enemy(grids[0]) + return True + if self.clear_roadblocks([ROAD_MAIN]): + return True + + grids = ROAD_MAIN.potential_roadblocks().select(is_accessible=True, enemy_scale=3) + if grids: + self.clear_chosen_enemy(grids[0]) + return True + if self.clear_potential_roadblocks([ROAD_MAIN]): + return True + + if self.clear_enemy(scale=(3,)): + return True + + grids = ROAD_MAIN.first_roadblock().select(is_accessible=True, enemy_scale=2) + if grids: + self.clear_chosen_enemy(grids[0]) + return True + if self.clear_enemy(scale=(2,)): + return True + + if self.clear_grids_for_faster(GRIDS_FOR_FASTER): + return True + + return self.battle_default() + + battle_1 = battle_0 + battle_2 = battle_0 + + def battle_3(self): + ignore = None + if self.fleet_at(A3, fleet=2): + ignore = SelectedGrids([A2]) + if self.fleet_at(G3, fleet=2): + ignore = SelectedGrids([H3]) + self.clear_all_mystery(nearby=False, ignore=ignore) + + if self.fleet_at(A3, fleet=2) and A2.is_mystery: + self.fleet_2.clear_chosen_mystery(A2) + if self.fleet_at(G3, fleet=2) and H3.is_mystery: + self.fleet_2.clear_chosen_mystery(H3) + + if self.map.select(is_mystery=True, is_accessible=False): + logger.info('Roadblock blocks mystery.') + if self.fleet_1.clear_roadblocks([ROAD_MAIN]): + return True + + if not self.map.select(is_mystery=True): + self.withdraw() diff --git a/campaign/event_20200227_cn/c2.py b/campaign/event_20200227_cn/c2.py new file mode 100644 index 000000000..8aee4d9dc --- /dev/null +++ b/campaign/event_20200227_cn/c2.py @@ -0,0 +1,100 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.campaign.assets import C2 as ENTRANCE +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + + +MAP = CampaignMap('c2') +MAP.shape = 'G7' +MAP.map_data = ''' + ME ++ -- -- ME -- MB + ME ++ ME ME ++ ++ -- + MS -- SP -- ++ ++ -- + -- ME -- -- -- -- -- + ME ++ ++ -- SP ME ME + -- ME ++ -- ME MS -- + MB -- -- -- ME -- ME +''' +MAP.weight_data = ''' + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 +''' +# MAP.camera_data = ['D3', 'D5'] +MAP.camera_data = ['C3', 'D5'] +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] + +A1, B1, C1, D1, E1, F1, G1, \ +A2, B2, C2, D2, E2, F2, G2, \ +A3, B3, C3, D3, E3, F3, G3, \ +A4, B4, C4, D4, E4, F4, G4, \ +A5, B5, C5, D5, E5, F5, G5, \ +A6, B6, C6, D6, E6, F6, G6, \ +A7, B7, C7, D7, E7, F7, G7, \ + = MAP.flatten() + +ROAD_MAIN = RoadGrids([[E6, E7, F5, G5]]) + + +class Config: + MAP_HAS_AMBUSH = False + CAMPAIGN_MODE = 'cd' + # INTERNAL_LINES_HOUGHLINES_THRESHOLD = 60 + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (150, 255 - 25), + 'width': 1, + 'prominence': 10, + 'distance': 50, + } + + +class Campaign(CampaignBase): + ENTRANCE = ENTRANCE + MAP = MAP + + def battle_0(self): + self.fleet_2.switch_to() + if self.clear_roadblocks([ROAD_MAIN]): + return True + if self.clear_siren(): + return True + + if self.clear_enemy(scale=(3,)): + return True + return self.battle_default() + + battle_1 = battle_0 + + def battle_2(self): + if self.clear_enemy(strongest=True): + return True + + return self.battle_default() + + battle_3 = battle_2 + + def battle_4(self): + + self.fleet_1.switch_to() + + return self.clear_boss() + + def handle_in_stage(self): + if self.appear(ENTRANCE): + logger.info('In stage.') + # self.device.sleep(0.5) + self.ensure_no_info_bar(timeout=0.6) + return True + else: + return False diff --git a/campaign/event_20200227_cn/d3.py b/campaign/event_20200227_cn/d3.py new file mode 100644 index 000000000..4929d05bf --- /dev/null +++ b/campaign/event_20200227_cn/d3.py @@ -0,0 +1,88 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.campaign.assets import D3 as ENTRANCE +from module.map.map_grids import SelectedGrids, RoadGrids + + +MAP = CampaignMap('d3') +MAP.shape = 'H7' +MAP.map_data = ''' + MS -- ++ MB MB ++ -- MS + -- ME ++ -- -- ++ ME -- + ME SP -- -- -- -- SP ME + -- -- ME ME -- ME -- -- + -- ++ ++ ++ -- ME -- ++ + ME -- ME ME -- ++ ME ME + -- ME -- ME MS -- ME -- +''' +MAP.weight_data = ''' + 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 +''' +MAP.camera_data = ['D3', 'D5', 'E5'] +# MAP.camera_data = ['D3', 'D5'] +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2, 'siren': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1}, + {'battle': 6, 'boss': 1}, +] + +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ + = MAP.flatten() + +ROAD_MAIN = RoadGrids([[A3, B2], [G2, H3]]) + + +class Config: + MAP_HAS_AMBUSH = False + CAMPAIGN_MODE = 'cd' + + +class Campaign(CampaignBase): + ENTRANCE = ENTRANCE + MAP = MAP + + def battle_0(self): + if self.clear_roadblocks([ROAD_MAIN]): + return True + if self.clear_siren(): + return True + + if self.clear_enemy(scale=(3,)): + return True + return self.battle_default() + + battle_1 = battle_0 + battle_2 = battle_0 + + def battle_3(self): + if self.clear_enemy(strongest=True): + return True + + return self.battle_default() + + battle_4 = battle_3 + + def battle_5(self): + if self.clear_enemy(scale=(2, 1)): + return True + + return self.battle_default() + + def battle_6(self): + return self.fleet_2.clear_boss() diff --git a/campaign/event_20200312_cn/sp3.py b/campaign/event_20200312_cn/sp3.py new file mode 100644 index 000000000..40ac95837 --- /dev/null +++ b/campaign/event_20200312_cn/sp3.py @@ -0,0 +1,96 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.campaign.assets import EVENT_20200312CN_SP3 as ENTRANCE +from module.map.map_grids import SelectedGrids, RoadGrids +from module.ui.assets import CAMPAIGN_GOTO_DAILY +from module.logger import logger + + +MAP = CampaignMap('d3') +MAP.shape = 'I6' +MAP.map_data = ''' + ++ MB -- ME -- ME -- ++ ++ + MB -- -- -- ++ -- ME ++ ++ + MB ME -- -- -- ME -- ME SP + ++ ++ ++ -- -- -- -- ++ -- + MB -- ME ME -- ++ -- ME -- + -- ME -- -- SP ++ -- -- -- + +''' +MAP.weight_data = ''' + 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 +''' +MAP.camera_data = ['D2', 'D4', 'F2', 'F4'] +# MAP.camera_data = ['D3', 'D5'] +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1}, + {'battle': 5, 'boss': 1}, +] + +A1, B1, C1, D1, E1, F1, G1, H1, I1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, \ + = MAP.flatten() + +ROAD_MAIN = RoadGrids([[B6, C5]]) + + +class Config: + MAP_HAS_AMBUSH = False + CAMPAIGN_MODE = 'normal' # A sp map can treat as normal map. + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (120, 255 - 40), + 'width': 2, + 'prominence': 10, + 'distance': 35, + } + + +class Campaign(CampaignBase): + ENTRANCE = ENTRANCE + MAP = MAP + + def battle_0(self): + if self.clear_roadblocks([ROAD_MAIN]): + return True + if self.clear_potential_roadblocks([ROAD_MAIN]): + return True + + if self.clear_enemy(scale=(1,)): + return True + if self.clear_enemy(scale=(2,)): + return True + + return self.battle_default() + + battle_1 = battle_0 + battle_2 = battle_0 + battle_3 = battle_0 + battle_4 = battle_0 + + def battle_5(self): + return self.fleet_2.clear_boss() + + def handle_in_stage(self): + """ + SP map don't have normal/hard switch button, + so overwrite handle_in_stage method in module.map.map_operation. + """ + if self.appear(CAMPAIGN_GOTO_DAILY) and self.appear(ENTRANCE): + logger.info('In stage.') + self.ensure_no_info_bar(timeout=0.6) + return True + else: + return False diff --git a/campaign/event_20200326_cn/a1.py b/campaign/event_20200326_cn/a1.py new file mode 100644 index 000000000..695945c07 --- /dev/null +++ b/campaign/event_20200326_cn/a1.py @@ -0,0 +1,66 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap() +MAP.shape = 'F7' +MAP.map_data = ''' + -- -- ME -- -- ++ + ME ++ ++ ME -- ME + -- ++ MS -- SP -- + MB MB __ -- -- ME + ME MB MS -- SP -- + -- ++ ME ME -- -- + ME ++ ME ME ++ ME +''' +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1, 'boss': 1}, +] + + +A1, B1, C1, D1, E1, F1, \ +A2, B2, C2, D2, E2, F2, \ +A3, B3, C3, D3, E3, F3, \ +A4, B4, C4, D4, E4, F4, \ +A5, B5, C5, D5, E5, F5, \ +A6, B6, C6, D6, E6, F6, \ +A7, B7, C7, D7, E7, F7, \ + = MAP.flatten() + + +class Config: + MAP_HAS_AMBUSH = False + CAMPAIGN_MODE = 'normal' + + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (50, 200), + 'width': 1, + 'prominence': 10, + 'distance': 35, + } + EDGE_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (255 - 80, 255), + 'prominence': 10, + 'distance': 50, + 'wlen': 1000 + } + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + if self.clear_boss(): + return True + + if self.clear_siren(): + return True + + if self.clear_enemy(scale=(3,)): + return True + + return self.battle_default() diff --git a/campaign/event_20200326_cn/a2.py b/campaign/event_20200326_cn/a2.py new file mode 100644 index 000000000..6470d0d95 --- /dev/null +++ b/campaign/event_20200326_cn/a2.py @@ -0,0 +1,52 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from campaign.event_20200326_cn.a1 import Config + +MAP = CampaignMap() +MAP.shape = 'F8' +MAP.map_data = ''' + -- ME -- ME ME ME + -- ME -- -- MS -- + ++ ++ ++ ME -- -- + SP -- -- -- -- SP + ME -- -- -- ++ ++ + -- MS ME __ ++ ++ + ME -- ++ -- -- MB + -- ME -- ME MB MB +''' +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] + + +A1, B1, C1, D1, E1, F1, \ +A2, B2, C2, D2, E2, F2, \ +A3, B3, C3, D3, E3, F3, \ +A4, B4, C4, D4, E4, F4, \ +A5, B5, C5, D5, E5, F5, \ +A6, B6, C6, D6, E6, F6, \ +A7, B7, C7, D7, E7, F7, \ +A8, B8, C8, D8, E8, F8, \ + = MAP.flatten() + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + if self.clear_boss(): + return True + + if self.clear_siren(): + return True + + if self.clear_enemy(scale=(3,)): + return True + + return self.battle_default() diff --git a/campaign/event_20200326_cn/a3.py b/campaign/event_20200326_cn/a3.py new file mode 100644 index 000000000..684af0287 --- /dev/null +++ b/campaign/event_20200326_cn/a3.py @@ -0,0 +1,59 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from campaign.event_20200326_cn.a1 import Config + +MAP = CampaignMap() +MAP.shape = 'G7' +MAP.map_data = ''' + ++ ++ ME ME ME ++ -- + ++ MS -- ME -- MS ++ + ME -- -- __ -- -- ME + ME -- SP MB SP -- ME + ME -- -- MB -- -- ME + -- MS -- MB ++ ++ ++ + ++ -- ME -- ME -- -- +''' +MAP.weight_data = """ + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 + 10 10 10 20 20 20 20 +""" +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 1}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 1, 'boss': 1}, +] + + +A1, B1, C1, D1, E1, F1, G1, \ +A2, B2, C2, D2, E2, F2, G2, \ +A3, B3, C3, D3, E3, F3, G3, \ +A4, B4, C4, D4, E4, F4, G4, \ +A5, B5, C5, D5, E5, F5, G5, \ +A6, B6, C6, D6, E6, F6, G6, \ +A7, B7, C7, D7, E7, F7, G7, \ + = MAP.flatten() + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + if self.clear_boss(): + return True + + if self.clear_siren(): + return True + + if self.clear_enemy(scale=(3,)): + return True + + return self.battle_default() diff --git a/campaign/event_20200326_cn/b1.py b/campaign/event_20200326_cn/b1.py new file mode 100644 index 000000000..7cd038a6d --- /dev/null +++ b/campaign/event_20200326_cn/b1.py @@ -0,0 +1,50 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from campaign.event_20200326_cn.a1 import Config + +MAP = CampaignMap() +MAP.shape = 'G7' +MAP.map_data = ''' + -- ME -- ME ++ ++ SP SP + ME -- MS -- ME -- -- SP + ++ ME -- -- -- -- -- -- + ++ ME -- -- MS -- ME ++ + ME -- __ ME ++ -- -- -- + MB -- -- MS ++ -- ME ME + MB MB ME -- ME -- ME -- +''' +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'boss': 1}, +] + + +A1, B1, C1, D1, E1, F1, G1, H1, \ +A2, B2, C2, D2, E2, F2, G2, H2, \ +A3, B3, C3, D3, E3, F3, G3, H3, \ +A4, B4, C4, D4, E4, F4, G4, H4, \ +A5, B5, C5, D5, E5, F5, G5, H5, \ +A6, B6, C6, D6, E6, F6, G6, H6, \ +A7, B7, C7, D7, E7, F7, G7, H7, \ + = MAP.flatten() + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + if self.clear_boss(): + return True + + if self.clear_siren(): + return True + + if self.clear_enemy(scale=(3,)): + return True + + return self.battle_default() diff --git a/campaign/event_20200326_cn/b2.py b/campaign/event_20200326_cn/b2.py new file mode 100644 index 000000000..05c239b23 --- /dev/null +++ b/campaign/event_20200326_cn/b2.py @@ -0,0 +1,58 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from campaign.event_20200326_cn.a1 import Config + +MAP = CampaignMap() +MAP.shape = 'F9' +MAP.map_data = ''' + ME ME MS MS ME ME + ME -- -- -- -- ME + ++ ++ -- -- ++ ++ + ++ ++ SP SP ME ++ + MS -- -- -- -- MS + ME -- MB MB __ ME + -- ME ++ ++ -- ME + -- ME ++ ME -- ME + -- -- ME -- -- ME +''' +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 1}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1, 'boss': 1}, +] + + +A1, B1, C1, D1, E1, F1, \ +A2, B2, C2, D2, E2, F2, \ +A3, B3, C3, D3, E3, F3, \ +A4, B4, C4, D4, E4, F4, \ +A5, B5, C5, D5, E5, F5, \ +A6, B6, C6, D6, E6, F6, \ +A7, B7, C7, D7, E7, F7, \ +A8, B8, C8, D8, E8, F8, \ +A9, B9, C9, D9, E9, F9, \ + = MAP.flatten() + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + if self.clear_boss(): + return True + + if self.clear_siren(): + return True + + if self.clear_enemy(scale=(3,)): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_2.clear_boss() diff --git a/campaign/event_20200326_cn/b3.py b/campaign/event_20200326_cn/b3.py new file mode 100644 index 000000000..7981ab41d --- /dev/null +++ b/campaign/event_20200326_cn/b3.py @@ -0,0 +1,55 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger +from campaign.event_20200326_cn.a1 import Config + +MAP = CampaignMap() +MAP.shape = 'G9' +MAP.map_data = ''' + ME -- ME -- ME -- ME + ++ -- -- ME -- -- ++ + ++ -- ++ ++ ++ -- ++ + MS -- MB MB MB -- MS + ME ME -- __ -- ME ME + ++ ++ SP -- SP ++ ++ + ++ ME -- -- -- ME ++ + ME -- -- ME -- -- ME + -- MS ME -- ME MS -- +''' +MAP.spawn_data = [ + {'battle': 0, 'enemy': 2, 'siren': 2}, + {'battle': 1, 'enemy': 1}, + {'battle': 2, 'enemy': 2}, + {'battle': 3, 'enemy': 1}, + {'battle': 4, 'enemy': 2}, + {'battle': 5, 'enemy': 1, 'boss': 1}, +] + + +A1, B1, C1, D1, E1, F1, G1, \ +A2, B2, C2, D2, E2, F2, G2, \ +A3, B3, C3, D3, E3, F3, G3, \ +A4, B4, C4, D4, E4, F4, G4, \ +A5, B5, C5, D5, E5, F5, G5, \ +A6, B6, C6, D6, E6, F6, G6, \ +A7, B7, C7, D7, E7, F7, G7, \ +A8, B8, C8, D8, E8, F8, G8, \ +A9, B9, C9, D9, E9, F9, G9, \ + = MAP.flatten() + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + if self.clear_siren(): + return True + + if self.clear_enemy(scale=(3,)): + return True + + return self.battle_default() + + def battle_5(self): + return self.fleet_2.clear_boss() diff --git a/cnocr_models/cnocr/models/label_cn.txt b/cnocr_models/cnocr/models/label_cn.txt new file mode 100644 index 000000000..c22d12eb6 --- /dev/null +++ b/cnocr_models/cnocr/models/label_cn.txt @@ -0,0 +1,6425 @@ +, +的 +。 +一 +是 +0 +不 +在 +有 +、 +人 +“ +” +了 +中 +国 +大 +为 +1 +: +上 +2 +这 +个 +以 +年 +生 +和 +我 +时 +之 +也 +来 +到 +要 +会 +学 +对 +业 +出 +行 +公 +能 +他 +于 +5 +e +3 +而 +发 +地 +可 +作 +就 +自 +们 +后 +成 +家 +日 +者 +分 +多 +下 +其 +用 +方 +本 +得 +子 +. +高 +4 +过 +经 +6 +现 +说 +与 +前 +o +理 +工 +所 +力 +t +如 +将 +军 +部 +, +事 +进 +9 +司 +场 +同 +机 +主 +都 +实 +天 +面 +市 +8 +i +a +新 +动 +开 +n +关 +定 +还 +长 +此 +月 +7 +道 +美 +心 +法 +最 +文 +等 +当 +第 +好 +然 +体 +全 +比 +股 +通 +性 +重 +三 +外 +s +但 +战 +; +相 +从 +你 +r +内 +无 +考 +因 +小 +资 +种 +合 +情 +去 +里 +化 +次 +入 +加 +间 +些 +度 +? +员 +意 +没 +产 +正 +表 +很 +队 +报 +已 +名 +海 +点 +目 +着 +应 +解 +那 +看 +数 +东 +位 +题 +利 +起 +二 +民 +提 +及 +明 +教 +问 +) +制 +期 +( +元 +游 +女 +- +并 +曰 +十 +果 +) +么 +注 +两 +专 +样 +信 +王 +平 +己 +金 +务 +使 +电 +网 +代 +手 +知 +计 +至 +常 +( +只 +展 +品 +更 +系 +科 +门 +特 +想 +西 +l +水 +做 +被 +北 +由 +c +》 +万 +老 +向 +《 +记 +政 +今 +据 +量 +保 +建 +物 +区 +管 +见 +安 +集 +或 +认 +程 +h +总 +— +少 +身 +先 +师 +球 +价 +空 +旅 +又 +求 +校 +强 +各 +非 +立 +受 +术 +基 +活 +反 +! +世 +何 +职 +导 +任 +取 +式 +[ +] +试 +才 +结 +费 +把 +收 +联 +直 +规 +持 +赛 +社 +四 +山 +统 +投 +南 +原 +该 +院 +交 +达 +接 +头 +打 +设 +每 +别 +示 +则 +调 +处 +义 +权 +台 +感 +斯 +证 +言 +五 +议 +d +给 +决 +论 +她 +告 +广 +企 +格 +增 +让 +指 +研 +商 +客 +太 +息 +近 +城 +变 +技 +医 +件 +几 +书 +选 +周 +备 +m +流 +士 +京 +传 +u +放 +病 +华 +单 +话 +招 +路 +界 +药 +回 +再 +% +服 +什 +改 +育 +口 +张 +需 +治 +德 +复 +准 +马 +习 +真 +语 +难 +始 +" +际 +观 +完 +标 +共 +项 +容 +级 +即 +必 +类 +领 +A +C +未 +w +型 +案 +线 +运 +历 +首 +风 +视 +色 +尔 +整 +质 +参 +较 +云 +具 +布 +组 +办 +气 +造 +争 +往 +形 +份 +防 +p +它 +车 +深 +神 +称 +g +况 +推 +越 +英 +易 +且 +营 +条 +消 +命 +团 +确 +S +划 +精 +足 +儿 +局 +飞 +究 +功 +索 +走 +望 +却 +查 +武 +思 +兵 +识 +克 +故 +步 +影 +带 +乐 +白 +源 +史 +航 +志 +州 +限 +清 +光 +装 +节 +号 +转 +图 +根 +省 +许 +引 +势 +失 +候 +济 +显 +百 +击 +f +器 +象 +效 +仅 +爱 +官 +包 +供 +低 +演 +连 +夫 +快 +续 +支 +验 +阳 +男 +觉 +花 +死 +字 +创 +素 +半 +预 +音 +户 +约 +率 +声 +请 +票 +… +便 +构 +T +存 +食 +y +段 +远 +责 +M +拉 +房 +随 +断 +极 +销 +林 +亚 +隐 +超 +获 +升 +B +采 +I +算 +益 +优 +愿 +找 +按 +维 +态 +满 +尽 +令 +汉 +委 +八 +终 +训 +值 +负 +境 +练 +母 +热 +适 +江 +住 +列 +举 +景 +置 +黄 +听 +除 +读 +众 +响 +友 +助 +弹 +干 +孩 +边 +李 +六 +甚 +罗 +致 +施 +模 +料 +火 +像 +古 +眼 +搜 +离 +D +闻 +府 +章 +早 +照 +速 +录 +页 +卫 +青 +例 +石 +父 +状 +农 +排 +降 +千 +P +择 +评 +疗 +班 +购 +属 +革 +够 +环 +占 +养 +曾 +米 +略 +站 +胜 +① +核 +否 +独 +护 +钱 +/ +红 +范 +另 +须 +余 +居 +虽 +毕 +攻 +族 +吃 +喜 +陈 +G +轻 +亲 +积 +星 +假 +b +县 +写 +刘 +财 +亿 +某 +括 +律 +酒 +策 +初 +批 +普 +片 +协 +售 +乃 +落 +留 +岁 +突 +双 +绝 +险 +季 +谓 +严 +村 +E +兴 +围 +依 +念 +苏 +底 +压 +破 +河 +怎 +细 +富 +切 +乎 +待 +室 +血 +帝 +君 +均 +络 +牌 +陆 +印 +层 +斗 +简 +讲 +买 +谈 +纪 +板 +希 +聘 +充 +归 +左 +测 +止 +笑 +差 +控 +担 +杀 +般 +朝 +监 +承 +播 +k +亦 +临 +银 +尼 +介 +v +博 +软 +欢 +害 +七 +良 +善 +’ +移 +土 +课 +免 +射 +审 +健 +角 +伊 +欲 +似 +配 +既 +拿 +刚 +绩 +密 +织 +九 +编 +狐 +右 +龙 +异 +若 +登 +检 +继 +析 +款 +纳 +威 +微 +域 +齐 +久 +宣 +阿 +俄 +店 +康 +执 +露 +香 +额 +紧 +培 +激 +卡 +短 +群 +② +春 +仍 +伤 +韩 +楚 +缺 +洲 +版 +答 +O +修 +媒 +秦 +‘ +错 +欧 +园 +减 +急 +叫 +诉 +述 +钟 +遇 +港 +补 +N +· +送 +托 +夜 +兰 +诸 +呢 +席 +尚 +福 +奖 +党 +坐 +巴 +毛 +察 +奇 +孙 +竞 +宁 +申 +L +疑 +黑 +劳 +脑 +R +舰 +晚 +盘 +征 +波 +背 +访 +互 +败 +苦 +阶 +味 +跟 +沙 +湾 +岛 +挥 +礼 +F +词 +宝 +券 +虑 +徐 +患 +贵 +换 +矣 +戏 +艺 +侯 +顾 +副 +妇 +董 +坚 +含 +授 +皇 +付 +坛 +皆 +抗 +藏 +潜 +封 +础 +材 +停 +判 +吸 +轮 +守 +涨 +派 +彩 +哪 +笔 +. +﹑ +氏 +尤 +逐 +冲 +询 +铁 +W +衣 +绍 +赵 +弟 +洋 +午 +奥 +昨 +雷 +耳 +谢 +乡 +追 +皮 +句 +刻 +油 +误 +宫 +巨 +架 +湖 +固 +痛 +楼 +杯 +套 +恐 +敢 +H +遂 +透 +薪 +婚 +困 +秀 +帮 +融 +鲁 +遗 +烈 +吗 +吴 +竟 +③ +惊 +幅 +温 +臣 +鲜 +画 +拥 +罪 +呼 +警 +卷 +松 +甲 +牛 +诺 +庭 +休 +圣 +馆 +_ +退 +莫 +讯 +渐 +熟 +肯 +V +冠 +谁 +乱 +朗 +怪 +夏 +危 +码 +跳 +卖 +签 +块 +盖 +束 +毒 +杨 +饮 +届 +序 +灵 +怀 +障 +永 +顺 +载 +倒 +姓 +丽 +靠 +概 +输 +货 +症 +避 +寻 +丰 +操 +针 +穿 +延 +敌 +悉 +召 +田 +稳 +典 +吧 +犯 +饭 +握 +染 +怕 +端 +央 +阴 +胡 +座 +著 +损 +借 +朋 +救 +库 +餐 +堂 +庆 +忽 +润 +迎 +亡 +肉 +静 +阅 +盛 +综 +木 +疾 +恶 +享 +妻 +厂 +杂 +刺 +秘 +僧 +幸 +扩 +裁 +佳 +趣 +智 +促 +弃 +伯 +吉 +宜 +剧 +野 +附 +距 +唐 +释 +草 +币 +骨 +弱 +俱 +顿 +散 +讨 +睡 +探 +郑 +频 +船 +虚 +途 +旧 +树 +掌 +遍 +予 +梦 +圳 +森 +泰 +慢 +牙 +盟 +挑 +键 +阵 +暴 +脱 +汇 +歌 +禁 +浪 +冷 +艇 +雅 +迷 +拜 +旦 +私 +您 +④ +启 +纷 +哈 +订 +折 +累 +玉 +脚 +亮 +晋 +祖 +菜 +鱼 +醒 +谋 +姐 +填 +纸 +泽 +戒 +床 +努 +液 +咨 +塞 +遭 +玩 +津 +伦 +夺 +辑 +癌 +x +丹 +荣 +仪 +献 +符 +翻 +估 +乘 +诚 +K +川 +惠 +涉 +街 +诗 +曲 +孔 +娘 +怒 +扬 +闲 +蒙 +尊 +坦 += +衡 +迪 +镇 +沉 +署 +妖 +脸 +净 +哥 +顶 +掉 +厚 +魏 +旗 +兄 +荐 +童 +剂 +乏 +倍 +萨 +偏 +洗 +惯 +灭 +径 +犹 +趋 +拍 +档 +罚 +纯 +洛 +毫 +梁 +雨 +瑞 +宗 +鼓 +辞 +洞 +秋 +郎 +舍 +蓝 +措 +篮 +贷 +佛 +坏 +俗 +殊 +炮 +厅 +筑 +姆 +译 +摄 +卒 +谷 +妈 +聚 +违 +忘 +鬼 +触 +丁 +羽 +贫 +刑 +岗 +庄 +伟 +兼 +乳 +叶 +凡 +龄 +宽 +峰 +宋 +硬 +岸 +迅 +喝 +拟 +雄 +役 +零 +舞 +暗 +潮 +绿 +倾 +详 +税 +酸 +徒 +伴 +诊 +跑 +吾 +燕 +澳 +啊 +塔 +宿 +恩 +忙 +督 +末 +⑤ ++ +伐 +篇 +敏 +贸 +巧 +截 +沟 +肝 +迹 +烟 +勇 +乌 +赞 +锋 +返 +迫 +凭 +虎 +朱 +拔 +援 +搞 +爆 +勤 +抢 +敬 +赶 +抱 +仁 +秒 +缓 +御 +唯 +缩 +尝 +贴 +奔 +跨 +炎 +汤 +侵 +骑 +励 +戴 +肤 +枪 +植 +瘤 +埃 +汽 +羊 +宾 +替 +幕 +贝 +刀 +映 +彻 +驻 +披 +抓 +奉 +抵 +肿 +麻 +U +炸 +繁 +赢 +茶 +伏 +梅 +狂 +忧 +豪 +暂 +贾 +洁 +绪 +刊 +忆 +桥 +晓 +册 +漫 +圆 +默 +妾 +侧 +址 +横 +□ +偶 +狗 +陵 +' +伙 +杜 +忍 +薄 +雪 +陷 +仙 +恋 +焦 +焉 +烦 +甘 +腺 +颇 +赏 +肠 +废 +墙 +债 +艾 +杰 +残 +冒 +屋 +堡 +曹 +储 +莱 +挂 +纵 +孝 +珍 +麦 +逃 +奋 +J +览 +镜 +缘 +昭 +摆 +跌 +胁 +昌 +耶 +腹 +偿 +蛋 +盈 +瓦 +摩 +沈 +惟 +迁 +冰 +辛 +震 +旁 +泉 +圈 +巡 +罢 +泛 +穷 +伸 +曼 +滋 +丈 +颜 +勒 +悲 +肥 +郭 +混 +灯 +租 +⑥ +鸡 +阻 +邑 +伍 +践 +驾 +魔 +X +拒 +懂 +糖 +脏 +沿 +翁 +胆 +惧 +聊 +携 +晨 +滑 +菌 +辅 +贤 +鉴 +丝 +尾 +赴 +吨 +宇 +眠 +脂 +籍 +彼 +污 +貌 +弄 +郡 +【 +奶 +菲 +烧 +垂 +壮 +浮 +弗 +赖 +】 +珠 +迟 +渠 +寿 +隆 +剑 +胞 +跃 +稍 +愈 +荷 +壁 +卿 +邦 +忠 +摇 +悟 +锦 +扰 +袭 +盾 +艘 +浓 +筹 +盗 +哭 +淡 +孕 +扣 +呈 +怨 +琳 +孤 +奴 +驱 +振 +闭 +~ +隔 +寒 +汝 +贯 +恢 +饰 +荡 +姑 +械 +* +猛 +亏 +锁 +硕 +舒 +嘉 +宏 +劲 +帅 +誉 +番 +惜 +胸 +抽 +脉 +孟 +遣 +碍 +辆 +玄 +陶 +丧 +矿 +链 +矛 +鸟 +夷 +嘴 +坡 +吕 +侦 +鸣 +妹 +邓 +钢 +妙 +z +欣 +骗 +浙 +辽 +奏 +唱 +腐 +仆 +祝 +冬 +韦 +邮 +酬 +尺 +涯 +毁 +粉 +井 +腰 +肌 +搭 +恨 +乙 +勿 +婆 +★ +闹 +猎 +厉 +哀 +递 +廉 +卧 +豆 +揭 +瓶 +⑦ +蒋 +忌 +贡 +邀 +覆 +墓 +捷 +Q +骂 +芳 +耗 +奈 +腾 +抑 +牵 +履 +绕 +睛 +炼 +描 +辉 +肃 +循 +仿 +葬 +漏 +恰 +殿 +遥 +尿 +凯 +仲 +婢 +胃 +翼 +卢 +慎 +厦 +颈 +哉 +疲 +惑 +汗 +衰 +剩 +昆 +耐 +疫 +霸 +赚 +彭 +狼 +洪 +枚 +媪 +纲 +窗 +偷 +鼻 +池 +磨 +尘 +账 +拼 +榜 +拨 +扫 +妆 +槽 +蔡 +扎 +叔 +辈 +― +泡 +伪 +邻 +锡 +仰 +寸 +盐 +叹 +囊 +幼 +拓 +郁 +桌 +舟 +丘 +棋 +裂 +扶 +逼 +熊 +轰 +允 +箱 +挺 +赤 +晶 +● +祭 +寄 +爷 +呆 +胶 +佩 +泪 +沃 +婴 +娱 +霍 +肾 +诱 +扁 +辩 +粗 +夕 +灾 +哲 +涂 +艰 +猪 +Y +铜 +踏 +赫 +吹 +屈 +谐 +仔 +沪 +殷 +辄 +渡 +屏 +悦 +漂 +祸 +赔 +涛 +谨 +赐 +劝 +泌 +凤 +庙 +墨 +寺 +淘 +勃 +崇 +灰 +虫 +逆 +闪 +竹 +疼 +旨 +旋 +蒂 +⑧ +悬 +紫 +慕 +贪 +慧 +腿 +赌 +捉 +疏 +卜 +漠 +堪 +廷 +氧 +牢 +吏 +帕 +棒 +纽 +荒 +屡 +戈 +氛 +黎 +桃 +幽 +尖 +猫 +捕 +嫁 +窃 +燃 +禽 +稿 +掩 +踪 +姻 +陪 +凉 +阔 +碰 +幻 +迈 +铺 +堆 +柔 +姿 +膜 +爸 +斤 +轨 +疆 +丢 +仓 +岂 +柳 +敦 +祥 +栏 +邪 +魂 +箭 +煤 +惨 +聪 +艳 +儒 +& +仇 +徽 +厌 +潘 +袖 +宅 +恒 +逻 +肺 +昂 +炒 +醉 +掘 +宪 +摸 +愤 +畅 +汪 +贺 +肪 +撑 +桂 +耀 +柏 +韂 +扑 +淮 +j +凌 +遵 +钻 +摘 +碎 +抛 +匹 +腔 +纠 +吐 +滚 +凝 +插 +鹰 +郊 +琴 +悄 +撤 +驶 +粮 +辱 +斩 +暖 +杭 +齿 +欺 +殖 +撞 +颁 +匈 +翔 +挤 +乔 +抚 +泥 +饱 +劣 +鞋 +肩 +雇 +驰 +莲 +岩 +酷 +玛 +赠 +斋 +辨 +泄 +姬 +拖 +湿 +滨 +鹏 +兽 +锐 +捧 +尸 +宰 +舆 +宠 +胎 +凶 +割 +虹 +俊 +糊 +兹 +瓜 +悔 +慰 +浦 +锻 +削 +唤 +戚 +撒 +冯 +丑 +亭 +寝 +嫌 +袁 +⑨ +尉 +芬 +挖 +弥 +喊 +纤 +辟 +菩 +埋 +呀 +昏 +傅 +桑 +稀 +帐 +添 +塑 +赋 +扮 +芯 +喷 +夸 +抬 +旺 +襄 +岭 +颗 +柱 +欠 +逢 +鼎 +苗 +庸 +甜 +贼 +烂 +怜 +盲 +浅 +霞 +畏 +诛 +倡 +磁 +茨 +毅 +鲍 +骇 +峡 +妨 +雕 +袋 +裕 +哩 +怖 +阁 +函 +浩 +侍 +拳 +寡 +鸿 +眉 +穆 +狱 +牧 +拦 +雾 +猜 +顷 +昔 +慈 +朴 +疯 +苍 +■ +渴 +慌 +绳 +闷 +陕 +宴 +辖 +「 +」 +舜 +讼 +柯 +丞 +姚 +崩 +绘 +枝 +牲 +涌 +虔 +姜 +擦 +桓 +逊 +汰 +斥 +﹒ +颖 +悠 +恼 +灌 +q +梯 +捐 +∶ +挣 +衷 +啡 +娜 +旬 +呵 +刷 +帽 +岳 +豫 +咖 +飘 +臂 +寂 +粒 +募 +嘱 +蔬 +苹 +泣 +吊 +淳 +诞 +诈 +咸 +猴 +~ +奸 +淫 +佐 +晰 +崔 +雍 +葛 +鼠 +爵 +奢 +仗 +涵 +淋 +挽 +敲 +沛 +蛇 +锅 +庞 +朵 +押 +鹿 +滩 +祠 +枕 +扭 +厘 +魅 +⑩ +湘 +柴 +炉 +荆 +卓 +碗 +夹 +脆 +颠 +窥 +逾 +诘 +贿 +虞 +茫 +榻 +碑 +傲 +骄 +卑 +× +Z +蓄 +煮 +劫 +卵 +碳 +痕 +攀 +搬 +拆 +谊 +禹 +窦 +绣 +叉 +爽 +肆 +羞 +爬 +泊 +腊 +愚 +牺 +胖 +弘 +秩 +娶 +妃 +柜 +觽 +躲 +葡 +浴 +兆 +滴 +衔 +燥 +斑 +挡 +笼 +徙 +憾 +垄 +肖 +溪 +叙 +茅 +膏 +甫 +缴 +姊 +逸 +淀 +擅 +催 +丛 +舌 +竭 +禅 +隶 +歧 +妥 +煌 +玻 +刃 +☆ +肚 +惩 +赂 +耻 +詹 +璃 +舱 +溃 +斜 +祀 +翰 +汁 +妄 +枭 +萄 +契 +骤 +醇 +泼 +咽 +拾 +廊 +犬 +筋 +扯 +狠 +挫 +钛 +扇 +蓬 +吞 +帆 +戎 +稽 +娃 +蜜 +庐 +盆 +胀 +乞 +堕 +趁 +吓 +框 +顽 +硅 +宛 +瘦 +剥 +睹 +烛 +晏 +巾 +狮 +辰 +茂 +○ +裙 +匆 +霉 +杖 +杆 +糟 +畜 +躁 +愁 +缠 +糕 +峻 +贱 +辣 +歼 +慨 +亨 +芝 +惕 +娇 +⑾ +渔 +冥 +咱 +栖 +浑 +禄 +帖 +巫 +喻 +毋 +泳 +饿 +尹 +穴 +沫 +串 +邹 +厕 +蒸 ++ +滞 +铃 +寓 +萧 +弯 +窝 +杏 +冻 +愉 +逝 +诣 +溢 +嘛 +兮 +暮 +豹 +骚 +跪 +懒 +缝 +盒 +亩 +寇 +弊 +巢 +咬 +粹 +冤 +陌 +涕 +翠 +勾 +拘 +侨 +肢 +裸 +恭 +叛 +纹 +摊 +# +兑 +萝 +饥 +> +浸 +叟 +滥 +灿 +衍 +喘 +吁 +晒 +谱 +堵 +暑 +撰 +棉 +蔽 +屠 +讳 +庶 +巩 +钩 +丸 +诏 +朔 +瞬 +抹 +矢 +浆 +蜀 +洒 +耕 +虏 +诵 +陛 +绵 +尴 +坤 +─ +尬 +搏 +钙 +饼 +枯 +灼 +饶 +杉 +盼 +蒲 +尧 +俘 +伞 +庚 +摧 +遮 +痴 +罕 +桶 +巷 +乖 +{ +啦 +纺 +闯 +→ +敛 +弓 +喉 +酿 +彪 +垃 +歇 +圾 +倦 +狭 +晕 +裤 +蜂 +} +垣 +莉 +谍 +俩 +妪 +⑿ +钓 +逛 +椅 +砖 +烤 +熬 +悼 +倘 +鸭 +馈 +惹 +旭 +薛 +诀 +渗 +痒 +蛮 +罩 +渊 +踢 +崖 +粟 +唇 +辐 +愧 +玲 +遏 +昼 +芦 +纣 +琼 +椎 +咳 +熙 +钉 +剖 +歉 +坠 +誓 +啤 +碧 +郅 +吻 +莎 +屯 +吟 +臭 +谦 +刮 +掠 +垫 +宙 +冀 +栗 +壳 +崛 +瑟 +哄 +谏 +丙 +叩 +缪 +雌 +叠 +奠 +髃 +碘 +暨 +劭 +霜 +妓 +厨 +脾 +俯 +槛 +芒 +沸 +盯 +坊 +咒 +觅 +剪 +遽 +贩 +寨 +铸 +炭 +绑 +蹈 +抄 +阎 +窄 +冈 +侈 +匿 +斌 +沾 +壤 +哨 +僵 +坎 +舅 +洽 +勉 +侣 +屿 +啼 +侠 +枢 +膝 +谒 +砍 +厢 +昧 +嫂 +羡 +铭 +碱 +棺 +漆 +睐 +缚 +谭 +溶 +烹 +雀 +擎 +棍 +瞄 +裹 +曝 +傻 +旱 +坑 +驴 +弦 +贬 +龟 +塘 +贞 +氨 +盎 +掷 +胺 +焚 +黏 +乒 +耍 +讶 +纱 +蠢 +掀 +藤 +蕴 +邯 +瘾 +婿 +卸 +斧 +鄙 +冕 +苑 +耿 +腻 +躺 +矩 +蝶 +浏 +壶 +凸 +臧 +墅 +粘 +⒀ +魄 +杞 +焰 +靶 +邵 +倚 +帘 +鞭 +僚 +酶 +靡 +虐 +阐 +韵 +迄 +樊 +畔 +钯 +菊 +亥 +嵌 +狄 +拱 +伺 +潭 +缆 +慑 +厮 +晃 +媚 +吵 +骃 +稷 +涅 +阪 +挨 +珊 +殆 +璞 +婉 +翟 +栋 +醋 +鹤 +椒 +囚 +瞒 +竖 +肴 +仕 +钦 +妒 +晴 +裔 +筛 +泻 +阙 +垒 +孰 +抖 +衬 +炫 +兢 +屑 +赦 +宵 +沮 +谎 +苟 +碌 +屁 +腕 +沦 +懈 +扉 +揖 +摔 +塌 +廖 +铝 +嘲 +胥 +曳 +敖 +傍 +筒 +朕 +扳 +鑫 +硝 +暇 +@ +冶 +靖 +袍 +凑 +悍 +兔 +邢 +熏 +株 +哮 +鹅 +乾 +鄂 +矶 +逵 +坟 +佣 +髓 +隙 +惭 +轴 +掏 +苛 +偃 +榴 +⒁ +赎 +谅 +裴 +缅 +皂 +淑 +噪 +阀 +咎 +揽 +绮 +瞻 +谜 +拐 +渭 +啥 +彦 +遁 +琐 +喧 +藉 +嫩 +寞 +梳 +溜 +粥 +恤 +迭 +瀑 +蓉 +寥 +彬 +俺 +忿 +螺 +膀 +惫 +扔 +匪 +毙 +怠 +彰 +啸 +荻 +逮 +删 +脊 +轩 +躬 +澡 +衫 +娥 +捆 +牡 +茎 +秉 +俭 +闺 +溺 +萍 +陋 +驳 +撼 +沽 +僮 +厥 +沧 +轿 +棘 +怡 +梭 +嗣 +凄 +℃ +铅 +绛 +祈 +斐 +箍 +爪 +琦 +惶 +刹 +嗜 +窜 +匠 +锤 +筵 +瑶 +幌 +捞 +敷 +酌 +阜 +哗 +聂 +絮 +阱 +膨 +坪 +歪 +旷 +翅 +揣 +樱 +甸 +颐 +兜 +頉 +伽 +绸 +拂 +狎 +颂 +谬 +昊 +皋 +嚷 +徊 +⒂ +曙 +麟 +嚣 +哑 +灞 +钧 +挪 +奎 +肇 +磊 +蕉 +荧 +嗽 +瓒 +苯 +躯 +绎 +鸦 +茵 +澜 +搅 +渺 +恕 +矫 +讽 +匀 +畴 +坞 +谥 +趟 +蔓 +帛 +寅 +呜 +枣 +萌 +磷 +涤 +蚀 +疮 +浊 +煎 +叮 +倩 +拯 +瑰 +涩 +绅 +枉 +朽 +哺 +邱 +凿 +莽 +隋 +炳 +睁 +澄 +厄 +惰 +粤 +黯 +纬 +哦 +徘 +炜 +擒 +捏 +帷 +攒 +湛 +夙 +滤 +浐 +霄 +豁 +甄 +剔 +丫 +愕 +袜 +呕 +| +蹲 +皱 +勘 +辜 +唬 +葱 +甩 +诡 +猿 +稻 +宦 +姨 +橡 +涧 +亢 +芽 +濒 +蹄 +窍 +譬 +驿 +拢 +叱 +喂 +怯 +坝 +椰 +孽 +阖 +瞩 +萎 +镑 +簿 +婷 +咐 +郸 +瑜 +瑚 +矮 +祷 +窟 +藩 +牟 +疡 +仑 +谣 +侄 +沐 +孜 +劈 +枸 +妮 +蔚 +勋 +玫 +虾 +谴 +莹 +紊 +瓷 +魁 +淄 +扛 +曩 +柄 +滔 +缀 +闽 +莞 +恳 +磅 +耸 +灶 +埠 +嚼 +汲 +恍 +逗 +畸 +翩 +甥 +蚁 +耽 +稚 +戟 +戊 +侃 +帜 +璧 +碟 +敞 +晖 +匙 +烫 +眷 +娟 +卦 +寐 +苌 +馨 +锣 +谛 +桐 +钥 +琅 +赁 +蜡 +颤 +陇 +僻 +埔 +腥 +皎 +酝 +媳 +⒃ +翘 +缔 +葫 +吼 +侮 +淹 +瘫 +窘 +啖 +犀 +弒 +蕾 +偕 +笃 +栽 +唾 +陀 +汾 +俨 +呐 +膳 +锌 +瞧 +骏 +笨 +琢 +踩 +濮 +黛 +墟 +蒿 +歹 +绰 +捍 +诫 +漓 +篷 +咄 +诬 +乓 +梨 +奕 +睿 +嫡 +幢 +砸 +俞 +亟 +捣 +溯 +饵 +嘘 +砂 +凰 +丕 +荥 +赀 +薇 +滕 +袱 +辍 +疹 +泗 +韧 +撕 +磕 +梗 +挚 +挠 +嫉 +奚 +弩 +蝉 +罐 +敝 +鞍 +晦 +酣 +搁 +柿 +菠 +卞 +煞 +堤 +蟹 +骼 +晤 +娡 +潇 +胰 +酱 +郦 +脖 +檐 +桩 +踵 +禾 +狩 +盏 +弈 +牒 +拙 +喇 +舶 +炊 +喀 +黔 +挟 +钞 +缕 +俏 +娄 +粪 +颅 +锏 +凹 +饲 +肘 +赟 +吝 +襟 +琪 +谕 +飙 +秽 +颊 +渝 +卯 +捡 +氢 +桀 +裳 +滇 +浇 +礁 +◎ +蚊 +芙 +荀 +吩 +凳 +峨 +巍 +雉 +郢 +铲 +倪 +杳 +汹 +豚 +乍 +蛙 +驼 +嗅 +讫 +痰 +棵 +睫 +绒 +捻 +罔 +杠 +氟 +堰 +羁 +穰 +钠 +骸 +睾 +鳞 +邸 +於 +谧 +睢 +泾 +芹 +钾 +颓 +Ⅱ +笋 +橘 +卉 +岐 +懿 +巅 +垮 +嵩 +柰 +鲨 +涡 +弧 +◆ +钝 +啃 +熹 +芭 +隅 +拌 +锥 +抒 +焕 +漳 +鸽 +烘 +瞪 +⒄ +箕 +驯 +恃 +靴 +刁 +聋 +剿 +筝 +绞 +鞅 +夯 +抉 +嘻 +弛 +垢 +衾 +丐 +斟 +恙 +雁 +匮 +娼 +鞠 +扼 +镶 +樵 +菇 +兖 +夭 +戌 +褚 +渲 +硫 +挞 +衙 +闫 +绾 +衅 +掣 +磋 +袒 +龚 +叨 +揉 +贻 +瑛 +俾 +薯 +憎 +傣 +炬 +荤 +烁 +沂 +粑 +蚌 +渣 +茄 +荼 +愍 +蒜 +菱 +狡 +蠡 +戍 +畤 +闵 +颍 +酋 +芮 +渎 +霆 +哼 +韬 +荫 +辙 +榄 +骆 +锂 +肛 +菑 +揪 +皖 +秃 +拽 +诟 +槐 +髦 +脓 +殡 +闾 +怅 +雯 +\ +戮 +澎 +悖 +嗓 +贮 +炙 +跋 +玮 +霖 +皓 +煽 +娠 +肋 +闸 +眩 +慷 +迂 +酉 +赘 +蝇 +羌 +蔑 +氯 +蚕 +汀 +憋 +臾 +汕 +缸 +棚 +唉 +棕 +裟 +蚡 +驮 +簇 +橙 +〉 +蹇 +庇 +佼 +禧 +崎 +痘 +芜 +姥 +绷 +惮 +雏 +⒅ +恬 +庵 +瞎 +臀 +胚 +嘶 +铀 +靳 +呻 +膺 +醛 +憧 +嫦 +橄 +褐 +讷 +趾 +讹 +鹊 +谯 +喋 +篡 +郝 +嗟 +琉 +逞 +袈 +鲧 +虢 +穗 +踰 +栓 +钊 +鬻 +羹 +掖 +笞 +恺 +掬 +憨 +狸 +瑕 +匡 +〈 +痪 +冢 +梧 +眺 +佑 +愣 +撇 +阏 +疚 +攘 +昕 +瓣 +烯 +谗 +隘 +酰 +绊 +鳌 +俟 +嫔 +崭 +妊 +雒 +荔 +毯 +纶 +祟 +爹 +辗 +竿 +裘 +犁 +柬 +恣 +阑 +榆 +翦 +佟 +钜 +札 +隧 +⒆ +腌 +砌 +酥 +辕 +铬 +痔 +讥 +毓 +橐 +跻 +酮 +殉 +哙 +亵 +锯 +糜 +壬 +瞭 +恻 +轲 +糙 +涿 +绚 +荟 +梢 +赣 +沼 +腑 +朦 +徇 +咋 +膊 +陡 +骋 +伶 +涓 +芷 +弋 +枫 +觑 +髻 +巳 +匣 +蠕 +恪 +槟 +栎 +噩 +葵 +殃 +淤 +诠 +昵 +眸 +馁 +奄 +绽 +闱 +蛛 +矜 +馔 +遐 +骡 +罹 +遑 +隍 +拭 +祁 +︰ +霁 +釜 +钵 +栾 +睦 +蚤 +咏 +憬 +韶 +圭 +觇 +芸 +氓 +伎 +氮 +靓 +淆 +绢 +眈 +掐 +簪 +搀 +玺 +镐 +竺 +峪 +冉 +拴 +忡 +卤 +撮 +胧 +邛 +彝 +楠 +缭 +棠 +腮 +祛 +棱 +睨 +嫖 +圉 +杵 +萃 +沁 +嬉 +擂 +澈 +麽 +轸 +彘 +褥 +廓 +狙 +笛 +彗 +啬 +盂 +贲 +忏 +驺 +悚 +豨 +旌 +娩 +扃 +蹦 +扈 +凛 +驹 +剃 +孺 +〕 +吆 +驷 +迸 +毗 +〔 +熔 +逍 +癸 +稼 +溥 +嫣 +瓮 +胱 +痊 +逡 +疟 +苻 +曪 +拣 +戛 +臻 +缉 +懊 +竣 +囤 +侑 +肽 +缮 +绥 +踝 +壑 +娴 +猝 +焻 +禀 +漱 +碁 +蹬 +祗 +濡 +挝 +亳 +萦 +癖 +彀 +毡 +锈 +憩 +筷 +莒 +噬 +珀 +砝 +鬓 +瑾 +澧 +栈 +恚 +搓 +褒 +疤 +沌 +絷 +镖 +塾 +钗 +骊 +拷 +铂 +郄 +窒 +驸 +裨 +矗 +烙 +惬 +炖 +赍 +迥 +蹴 +炽 +诧 +闰 +糯 +捅 +茜 +漯 +﹐ +峭 +哇 +鹑 +疵 +梓 +骠 +咫 +鹦 +檀 +痹 +侥 +蘑 +衢 +灸 +琵 +琶 +懦 +邺 +扪 +痿 +苔 +拇 +腋 +薨 +馅 +雠 +敕 +捂 +鴈 +栅 +瓯 +嘿 +溉 +胳 +拎 +巿 +赃 +咕 +诃 +谤 +舁 +禺 +榨 +– +拈 +瘙 +眯 +篱 +鬟 +咯 +抨 +桨 +岱 +赡 +蹶 +惚 +嗔 +喏 +聆 +曜 +窑 +瘢 +柠 +蕃 +寤 +攫 +饷 +佬 +臼 +皈 +蟒 +啜 +蔗 +汶 +酪 +豕 +窖 +膛 +檬 +戾 +蟠 +黍 +鲸 +漾 +猾 +驭 +踊 +稠 +脯 +潍 +倭 +谑 +猖 +聒 +骞 +熄 +渍 +瞳 +蒯 +陉 +褪 +筐 +彤 +蝴 +廪 +嬴 +沱 +闼 +橱 +蜚 +蹭 +鄢 +臆 +邳 +盔 +眶 +沓 +飨 +覃 +彷 +淌 +岚 +霹 +辔 +袂 +嗤 +榔 +鸾 +綦 +莘 +媲 +翊 +雳 +箸 +蚩 +茸 +嗦 +楷 +韭 +簸 +帚 +坍 +後 +璋 +剽 +渤 +骥 +犊 +迩 +悯 +饪 +搂 +鹉 +岑 +觞 +棣 +蕊 +诳 +黥 +藻 +郜 +舵 +毂 +茗 +忱 +铿 +谙 +怆 +钳 +佗 +瀚 +亘 +铎 +咀 +濯 +鼾 +酵 +酯 +麾 +Ⅰ +笙 +ü +缨 +翳 +龈 +忒 +煦 +顼 +俎 +圃 +刍 +喙 +羲 +陨 +嘤 +梏 +颛 +蜒 +啮 +镁 +辇 +葆 +蔺 +筮 +溅 +佚 +匾 +暄 +谀 +媵 +纫 +砀 +悸 +啪 +迢 +瞽 +莓 +瞰 +俸 +珑 +骜 +穹 +麓 +潢 +妞 +铢 +忻 +铤 +劾 +樟 +俐 +缗 +煲 +粱 +虱 +淇 +徼 +脐 +鼋 +嘈 +悴 +捶 +嚏 +挛 +谚 +螃 +殴 +瘟 +掺 +〇 +酚 +梵 +栩 +褂 +摹 +蜿 +钮 +箧 +胫 +馒 +焱 +嘟 +芋 +踌 +圜 +衿 +峙 +宓 +腆 +佞 +砺 +婪 +瀛 +苷 +昱 +贰 +秤 +扒 +龁 +躇 +翡 +宥 +弼 +醮 +缤 +瘗 +鳖 +擞 +眨 +礶 +锢 +辫 +儋 +纭 +洼 +漕 +飓 +纂 +繇 +舷 +勺 +诲 +捺 +瞑 +啻 +蹙 +佯 +茹 +怏 +蛟 +鹭 +烬 + +兀 +檄 +浒 +胤 +踞 +僖 +卬 +爇 +璀 +暧 +髡 +蚂 +饽 +镰 +陂 +瞌 +诽 +钺 +沥 +镍 +耘 +燎 +祚 +儣 +莺 +屎 +辘 +鸥 +驩 +氐 +匕 +銮 +━ +苴 +憔 +渥 +袅 +瞿 +瓢 +痣 +蘸 +蹑 +玷 +惺 +轧 +喃 +潺 +唏 +逅 +懵 +帏 +唠 +徨 +咤 +抠 +蛊 +苇 +铮 +疙 +闳 +砥 +羸 +遨 +哎 +捽 +钏 +壹 +昇 +擢 +贽 +汴 +砰 +牝 +蔼 +熠 +粽 +绌 +杼 +麒 +叭 +颔 +锭 +妍 +姒 +邂 +濞 +轶 +搔 +蹊 +阂 +垦 +猕 +伫 +瘩 +璐 +黠 +婺 +噫 +潞 +呱 +幡 +汞 +缯 +骁 +墩 +赧 +瞥 +媛 +瞠 +羔 +轼 +Ⅲ +拗 +鹞 +搴 +诮 +趴 +凋 +撩 +芥 +缎 +摒 +泮 +惘 +骛 +瘳 +姝 +β +渚 +吠 +稣 +獘 +篃 +罄 +吒 +茧 +黜 +缢 +獗 +诅 +絜 +蜕 +屹 +哽 +缄 +俑 +坷 +杓 +剁 +锺 +鹜 +谩 +岔 +籽 +磬 +溍 +邃 +钨 +甬 +笥 +蝠 +龋 +鸱 +孚 +馍 +溴 +妫 +偎 +烽 +椽 +阮 +酗 +惋 +牍 +觥 +瞅 +涣 +狈 +锰 +椟 +饺 +溲 +谪 +掇 +蓟 +倔 +鞫 +猢 +笄 +翕 +嗥 +卺 +寰 +狞 +洮 +炕 +夡 +瘠 +磺 +肱 +奭 +耆 +棂 +娅 +咚 +豌 +樗 +诩 +斡 +榈 +琛 +狲 +蕲 +捎 +戳 +炯 +峦 +嘎 +睬 +怙 +疱 +霎 +哂 +鱿 +涸 +咦 +痉 +$ +抟 +庖 +沅 +瑙 +珏 +祜 +楞 +漉 +鸠 +镂 +诰 +谄 +蜗 +嗒 +珂 +祯 +鸳 +殒 +潼 +柩 +萤 +柑 +轵 +缰 +淼 +冗 +蕙 +鳄 +嘀 +彊 +峥 +雹 +藜 +笠 +岖 +傥 +潦 +苞 +蛰 +嬖 +僦 +碣 +裰 +疸 +湮 +昴 +榷 +涎 +攸 +砾 +跖 +恂 +舄 +麝 +貂 +孢 +捋 +笈 +璨 +粕 +浚 +鹃 +歆 +漪 +岷 +咧 +殁 +篆 +湃 +侏 +傈 +殇 +霭 +嚎 +拊 +崂 +鬲 +碉 +菁 +庾 +拚 +旃 +幺 +皿 +焊 +噢 +祺 +锚 +痤 +翎 +醺 +噶 +傀 +俛 +秧 +谆 +僳 +菽 +绯 +瘥 +盥 +蹋 +髯 +岌 +痧 +偌 +禳 +簧 +跤 +伉 +腼 +爰 +箫 +曦 +蜘 +霓 +愆 +姗 +陬 +楂 +嵘 +蜓 +浼 +癫 +瓠 +跷 +绐 +枷 +墀 +馕 +盹 +聩 +镯 +砚 +晁 +僊 +° +坂 +煜 +俚 +眛 +焘 +阍 +袄 +夔 +馋 +泸 +庠 +毐 +飚 +刭 +琏 +羿 +斓 +稔 +阉 +喾 +恸 +耦 +咪 +蝎 +唿 +桔 +缑 +诋 +訾 +迨 +鹄 +蟾 +鬣 +廿 +莅 +荞 +槌 +媾 +愦 +郏 +淖 +嗪 +镀 +畦 +颦 +浃 +牖 +襁 +怂 +唆 +嚭 +涟 +拮 +腓 +缥 +郫 +遴 +邾 +悒 +嗝 +殽 +跛 +掂 +撬 +鄣 +鄱 +斫 +窿 +兕 +壕 +疽 +铙 +吱 +厩 +甭 +镪 +篝 +踣 +眦 +啧 +糠 +鲤 +粲 +噱 +椭 +哟 +潸 +铆 +姣 +馥 +胙 +迦 +偻 +嗯 +陟 +爲 +桧 +鸯 +恿 +晌 +臱 +骈 +喽 +淅 +澹 +叽 +桢 +刨 +忑 +忐 +猩 +蝙 +旄 +晾 +吭 +荏 +觐 +胄 +榛 +豢 +堑 +帔 +咙 +柚 +僭 +锵 +√ +肮 +囿 +忤 +惴 +燮 +棹 +摈 +缈 +幛 +墉 +诎 +仞 +剌 +氇 +泯 +茱 +獾 +豺 +蜃 +殂 +窈 +倨 +褓 +詈 +砷 +邕 +薰 +頫 +焖 +痫 +痢 +掾 +獐 +簌 +雎 +é +帧 +鸩 +匝 +桅 +椁 +绫 +桡 +氆 +哌 +咛 +鞘 +辎 +缙 +玑 +佤 +垓 +槿 +蛤 +烨 +泓 +罴 +鄜 +褶 +瘀 +颌 +蹂 +弑 +珪 +曷 +膑 +惦 +咆 +梆 +蛾 +牂 +髅 +捱 +拧 +婧 +踱 +怵 +侗 +屉 +讪 +衲 +麋 +宕 +畿 +唧 +怛 +豉 +籁 +觌 +舂 +蓦 +廨 +胪 +怍 +鄄 +绶 +飕 +蜻 +欷 +邬 +杲 +汧 +唑 +冽 +邰 +鼍 +魇 +铐 +哝 +泱 +扞 +飒 +醴 +陲 +喟 +筠 +殓 +瘸 +倏 +嗳 +啕 +睑 +翌 +à +幄 +娓 +蓺 +妩 +奁 +璜 +桦 +朐 +榕 +礴 +儡 +婕 +觎 +觊 +绦 +猥 +涮 +倬 +袤 +啄 +掳 +椿 +俪 +噜 +摞 +※ +鄗 +漩 +悝 +淞 +袴 +僇 +酹 +搒 +跽 +鳍 +疣 +姁 +猗 +舛 +鞮 +砭 +郯 +徕 +纥 +梃 +卮 +肣 +湎 +怦 +揄 +迕 +芍 +珥 +羚 +喔 +缁 +涝 +栉 +犷 +汜 +悻 +呛 +赭 +淬 +泫 +炀 +箴 +镌 +髫 +拄 +怔 +炷 +桎 +巽 +汭 +鹫 +挈 +蝄 +噙 +锄 +邴 +歔 +瘪 +腴 +呗 +慵 +撺 +欤 +阡 +傩 +苫 +掰 +盅 +冑 +躏 +茉 +霾 +耄 +楹 +蹻 +苋 +鲠 +哆 +傒 +榭 +牦 +婶 +仃 +囱 +皙 +醦 +隰 +掼 +琖 +駆 +暲 +砒 +舀 +鹗 +犒 +斛 +甑 +楫 +嫪 +胭 +瘁 +铛 +藕 +簋 +腭 +睽 +阕 +裀 +砧 +蓼 +贳 +劬 +搽 +龏 +荃 +奘 +祎 +泵 +攥 +翱 +晟 +酎 +睇 +逋 +箔 +羟 +诙 +饬 +跆 +眇 +佻 +铠 +娑 +郧 +葭 +蝗 +郓 +幞 +鉏 +碾 +硒 +釉 +磔 +殄 +藐 +莠 +颧 +熨 +獠 +浞 +笺 +癣 +茬 +衽 +喳 +裾 +倜 +鸢 +蠹 +廛 +惆 +芈 +燔 +伛 +妗 +佃 +缜 +咣 +龛 +挎 +徵 +粼 +锉 +啾 +隼 +猬 +镳 +璇 +胯 +饕 +揩 +縠 +虮 +苓 +噎 +祓 +筰 +奂 +搪 +喁 +俦 +隗 +馏 +圩 +褫 +僰 +吮 +哧 +湫 +旻 +筏 +搢 +佶 +茕 +铣 +娆 +揍 +嗷 +柈 +蕨 +绖 +旎 +汨 +畑 +鳏 +厝 +溷 +楯 +卅 +祇 +′ +怼 +焯 +± +柘 +骷 +澍 +▲ +` +珞 +褊 +╱ +痂 +罘 +殚 +垠 +缧 +瑁 +齮 +蓐 +怿 +蹿 +豳 +犴 +孵 +筱 +蜷 +窋 +泞 +肄 +祐 +窕 +酆 +谶 +阗 +讙 +镝 +匍 +腱 +^ +镬 +仡 +樾 +驽 +峒 +蟆 +葳 +徉 +昙 +罡 +耜 +嗨 +氲 +骅 +襦 +浔 +纮 +洱 +氦 +舐 +黙 +臊 +縯 +汛 +蹀 +溟 +枥 +祉 +铄 +豸 +揶 +馀 +闇 +呷 +仄 +焒 +嗡 +崆 +匳 +皑 +匐 +÷ +诿 +髭 +鲰 +鲲 +筴 +侬 +鹳 +滂 +△ +橹 +邈 +弭 +弁 +樽 +揆 +幔 +纨 +踉 +帼 +跸 +搠 +缞 +氤 +旒 +旖 +屣 +孱 +槁 +铉 +榼 +沣 +娣 +娈 +夤 +壅 +枇 +讴 +埶 +阆 +杷 +浣 +狰 +愠 +蚓 +咿 +藿 +欻 +萸 +刽 +稞 +刎 +骖 +冁 +骰 +嵯 +濂 +跚 +湄 +釂 +麤 +珰 +舔 +谮 +坨 +嗲 +埒 +锲 +鲇 +煨 +耎 +绻 +楣 +噉 +谟 +嗖 +裆 +晗 +囹 +黝 +讣 +薏 +⑴ +貉 +椹 +蟜 +犍 +蜇 +秏 +呶 +箩 +悞 +妤 +搐 +芪 +呦 +恽 +赊 +侩 +绁 +猱 +遒 +镵 +鸮 +趺 +簏 +迤 +坼 +痼 +棰 +凫 +诂 +骀 +瘴 +螨 +阚 +臃 +葩 +篓 +谲 +悌 +嬗 +颉 +赉 +珈 +汩 +薮 +亶 +鬃 +蒽 +黾 +噤 +螫 +嶲 +湍 +畲 +徜 +衮 +茀 +蓍 +┐ +遛 +磐 +篁 +遘 +乩 +蹒 +≥ +鸵 +褴 +苒 +郈 +踽 +叵 +咻 +伋 +襆 +歙 +伧 +醳 +鄠 +茴 +赳 +矾 +圄 +楮 +坯 +蕤 +迓 +锱 +腉 +滦 +饯 +诤 +懋 +呤 +纡 +隽 +妲 +蜴 +┌ +疋 +噻 +愀 +龊 +琨 +镭 +藓 +镣 +滈 +蓓 +杪 +糗 +菅 +椀 +懑 +苎 +劓 +囫 +α +啰 +钼 +烷 +兒 +脔 +郴 +忖 +芎 +啶 +巉 +钒 +缒 +蝼 +龌 +沔 +醢 +晔 +孳 +忝 +嗫 +橇 +勖 +宸 +佰 +蜈 +酞 +蔷 +糅 +噭 +猊 +儇 +觳 +缟 +郐 +眙 +赅 +剜 +徭 +蛭 +愎 +唔 +瘘 +魋 +镉 +殛 +茏 +邋 +垛 +垩 +焙 +篾 +羯 +浍 +鏖 +嚓 +躞 +堃 +烩 +莴 +¥ +绠 +纔 +衩 +糁 +≤ +町 +粝 +玳 +穑 +葺 +钲 +徂 +﹖ +棓 +泷 +涪 +囵 +怫 +屦 +歘 +鐘 +『 +裱 +缱 +圹 +罂 +荦 +腈 +愬 +坭 +嗛 +铩 +馐 +媸 +遢 +て +渑 +曛 +粳 +蹰 +舫 +勐 +窭 +濠 +亹 +跄 +琥 +戢 +駹 +燧 +嫜 +峄 +竽 +膈 +荚 +姞 +赇 +樭 +澙 +笮 +嶙 +氰 +孀 +崧 +郾 +蜥 +阊 +篙 +狻 +靛 +虬 +赝 +篑 +榇 +鞑 +侪 +盍 +疝 +矽 +堙 +毶 +泠 +瞟 +癀 +镞 +酤 +涔 +譄 +唁 +薜 +郿 +⑵ +爻 +盱 +膻 +菡 +⒉ +绨 +埽 +О +鳜 +醚 +阃 +遶 +岿 +張 +椐 +酺 +蔟 +螂 +辂 +窠 +淙 +鷪 +貋 +刳 +骶 +恫 +挹 +婀 +铳 +蒍 +孥 +蚣 +唳 +纻 +Ⅳ +甾 +旘 +膘 +< +脍 +耨 +翮 +赈 +浜 +洹 +蛎 +魉 +纰 +岫 +坌 +捭 +睒 +轺 +锗 +稗 +崚 +仫 +珩 +庑 +邽 +麃 +』 +縻 + +嗑 +瞋 +螭 +绔 +喱 +‰ +痞 +咔 +埤 +疥 +猷 +洺 +啁 +讦 +礻 +餮 +泅 +蛹 +癞 +妁 +桞 +匏 +琮 +铨 +杌 +孑 +菟 +骐 +钡 +钚 +莆 +荪 +魑 +峇 +斄 +缶 +茭 +煅 +酩 +酢 +湟 +潏 +嘌 +韪 +苣 +蛆 +侔 +帑 +鸨 +愫 +芫 +郪 +踔 +骧 +茁 +溧 +皁 +蜔 +魍 +瀹 +楔 +祧 +粜 +晡 +蹩 +畎 +啱 +窳 +瞾 +甙 + + +绺 +貔 + +痈 +舡 +葴 +耋 +囔 +П +蚯 +笆 +鲐 +踧 +遫 +踟 +Р +溊 +咂 +锹 +笫 +癔 +觜 +涒 +碓 +蛲 +跺 +枞 +茔 +⒈ +谸 +抿 +擘 +跬 +愛 +浿 +∩ +黟 +枰 +な +轘 +荠 +郇 +姮 +锑 +妳 +饴 +绡 +奡 +夥 +钤 +俅 +酊 +潴 +绀 +髋 +獬 +儆 +産 +乂 +餍 +颡 +胾 +碛 +貊 +魭 +钿 +鸬 +喑 +哏 +牯 +蜍 +摁 +嶓 +俳 +蟭 +躅 +羖 +鳃 +孛 +羑 +濑 +雩 +焜 +鸷 +箦 +茯 +醪 +鹂 +铚 +缳 +螳 +酇 +蛔 +罃 +珐 +苕 +罅 +蛀 +庳 +褛 +罥 +艮 +娲 +蒺 +娉 +撵 +禨 +蓖 +姹 +戕 +庥 +岬 +痍 +烜 +窴 +邠 +蹉 +诨 +狁 +顒 +莨 +阈 +嘹 +戆 +窎 +儙 +螾 +纾 +嵋 +镕 +跣 +繻 +枳 +菏 +赜 +槃 +趄 +煊 +嬛 +抡 +睚 +跹 +壖 +戗 +⑶ +榫 +沬 +崴 +颚 +畼 +嫚 +嚋 +珮 +◇ +娀 +枋 +獭 +畀 +谇 +欃 +瓴 +龂 +鲋 +鹆 +鳝 +郕 +疴 +偈 +诒 +讧 +惇 +跂 +扢 +爨 +赪 +苡 +鈇 +晞 +亓 +釐 +槊 +寘 +暾 +莩 +徳 +钹 +冏 +書 +麂 +撂 +犨 +滁 +孪 +刓 +逶 +澝 +嬃 +黡 +沕 +恝 +洟 +秸 +逑 +滓 +緃 +媢 +叼 +霣 +⒊ +慝 +厍 +炟 +皤 +囐 + +硼 +楸 +瞀 +烝 +炔 +瓻 +耙 +腩 +醵 +锽 +殪 +樯 +芡 +∈ +↓ +缵 +伻 +玊 +桠 +觚 +踯 +噔 +碴 +砣 +忪 +藁 +镒 +佝 +峤 +峣 +搤 +汐 +嗾 +鞚 +巂 +楗 +呓 +狒 +開 +坻 +蘧 +趵 +榱 +锷 +锾 +隳 +饟 +饦 +馎 +驵 +骘 +髀 +髑 +鮼 +鲑 +鲔 +鹘 +鹚 +﹔ +│ +刈 +刖 +剎 +啐 +嘭 +噌 +噗 +嚬 +嚰 +圯 +坳 +嫄 +寖 +尻 +峋 +崃 +嶂 +嶶 +帇 +幤 +悫 +慙 +扌 +揜 +撝 +旳 +昀 +昃 +暹 +玕 +琰 +璆 +玃 +疃 +猃 +皴 +狃 +祊 +燹 +燠 +熛 +窣 +窬 +糌 +糍 +紬 +濩 +飧 +肸 +脲 +臬 +芘 +荜 +蔫 +襜 +觖 +豭 +贇 +氩 +氖 +趸 +檠 +檇 +邘 +鄏 +酡 +鑙 +钴 +铵 +氅 +莜 +柢 +悭 +鄳 +蒗 +虺 +沇 +薤 +踹 +墠 +唶 +骍 +镊 +镛 +帨 +逖 +氡 +鹣 +恹 +臛 +呃 +幂 +鹖 +間 +磛 +弢 +蛐 +懜 +凇 +闟 +璟 +遹 +肓 +剐 +垝 +杅 +笤 +佈 +撷 +佘 +嚅 +蝮 +谳 +蚝 +栀 +眢 +∵ +蓿 +枵 +橪 +騳 +≠ +蟋 +嗌 +玦 +嗄 +劙 +騠 +鞣 +唢 +茆 +蚰 +喹 +趱 +珅 +喆 +谔 +苄 +靥 +鲛 +洫 +颀 +趹 +蛩 +馓 +轫 +叡 +蒉 +睪 +漦 +胝 +瘐 +逦 +嶷 +傕 +斲 +嵬 +缇 +洙 +瘵 +縢 +渖 +價 +灊 +訇 +醍 +膦 +癜 +歃 +钎 +讵 +钰 +嫱 +婊 +狝 +榧 +脁 +柞 +玟 +迳 +仝 +鸪 +椋 +嵊 +祢 +螟 +淦 +穸 +舸 +铡 +肼 +鲷 +琊 +岘 +霰 +菖 +邗 +萱 +骺 +洵 +獒 +砻 +涠 +炅 +樨 +戬 +铑 +桉 +鳐 +朊 +浠 +圻 +楝 +茼 +荬 +铱 +箬 +鳟 +铧 +涞 +椤 +捌 +鲶 +泺 +锛 +! +钇 +椴 +踮 +崤 +洄 +郛 +溆 +¢ +柒 +锝 +楦 +玖 +铋 +髌 +韫 +璺 +酐 +碲 +鲟 +呋 +鹧 +婵 +淝 +耒 +〓 +铰 +馊 +缂 +溏 +颞 +耱 +邡 +萋 +崮 +吲 +氵 +嗬 +旮 +镓 +浈 +瑷 +肟 +仟 +宀 +瀵 +囡 +绱 +瘿 +蠖 +镗 +鲵 +锶 +绉 +嘞 +鲂 +铌 +铟 +胍 +疳 +氘 +墒 +叁 +螯 +髁 +狍 +葑 +槎 +鳗 +麸 +耧 +唰 +灬 +乜 +啵 +钽 +谡 +垸 +锴 +眭 +濉 +钬 +箐 +丿 +荽 +丶 +桷 +垅 +缦 +哞 +昝 +氽 +瑗 +烊 +犟 +莳 +绂 +锆 +溱 +埂 +萘 +鳕 +珉 +绋 +秣 +〒 +逯 +缍 +镱 +鄯 +晷 +庹 +甯 +茌 +珲 +旯 +霏 +埭 +峁 +岢 +刿 +掸 +馄 +蝌 +堇 +桁 +嗉 +杩 +妯 +阄 +镧 +窨 +脒 +馗 +嵇 +瓤 +〃 +岙 +? +埚 +砦 +辋 +倌 +锕 +笏 +杈 +蟀 +鲢 +尕 +铈 +〖 +蚬 +挲 +疖 +铕 +苘 +秕 +璩 +塍 +鲈 +镦 +谌 +菀 +哒 +冼 +崽 +钕 +灏 +磴 +榉 +掴 +嫒 +唛 +轭 +姘 +寮 +篪 +赓 +娌 +蚜 +蚴 +吖 +屐 +々 +£ +瓿 +铍 +缬 +鲆 +鲫 +扦 +萼 +碚 +蜊 +噼 +蟑 +颢 +胴 +羰 +唷 +籼 +萁 +蕹 +吡 +芾 +蒡 +蚪 +哓 +蹼 +埕 +鲽 +蔻 +擀 +〗 +硖 +秭 +悱 +氙 +辊 +§ +哚 +鄞 +硷 +璁 +谘 +垌 +鼬 +跎 +毽 +蛴 +沤 +瀣 +缃 +蛏 +逄 +笕 +蜱 +仨 +沭 +苁 +蘖 +檩 +琚 +滢 +呸 +饨 +耷 +氚 +纛 +鲳 +滟 +巯 +薹 +诶 +褰 +锔 +蚶 +钣 +泔 +菪 +醐 +塬 +垭 +嘧 +荸 +畈 +鲅 +锟 +邝 +皲 +卟 +畹 +莼 +亍 +汊 +渌 +螈 +琬 +铒 +脘 +蝈 +橼 +醌 +喵 +蝾 +煸 +痱 +绲 +亻 +熵 +〆 +叻 +鎏 +鳅 +郗 +啉 +谰 +烃 +聿 +蛳 +昶 +缫 +荭 +囟 +蠓 +蒌 +苜 +稹 +炝 +艄 +掊 +髂 +撅 +宄 +勰 +蓑 +鹌 +謇 +莪 +蝽 +芗 +萜 +蔸 +痨 +胛 +绗 +剡 +羧 +芩 +檗 +蕈 +澶 +砜 +嬷 +铖 +蚱 +沏 +貅 +芊 +囗 +砼 +撸 +艹 +豇 +ˉ +璎 +鳙 +虻 +蓥 +荨 +咩 +珙 +坩 +胗 +砬 +秆 +垡 +鳎 +铊 +蚧 +矸 +逭 +蘼 +硪 +胬 +孓 +碇 +哔 +艿 +堀 +槲 +潋 +屌 +← +脒 +蚺 +鳢 +鲱 +靼 +嵴 +硐 +钌 +钪 +: +; +丨 +‖ +ˇ + \ No newline at end of file diff --git a/cnocr_models/cnocr/models/model-v1.0.0-0020.params b/cnocr_models/cnocr/models/model-v1.0.0-0020.params new file mode 100644 index 000000000..6481612e5 Binary files /dev/null and b/cnocr_models/cnocr/models/model-v1.0.0-0020.params differ diff --git a/cnocr_models/cnocr/models/model-v1.0.0-symbol.json b/cnocr_models/cnocr/models/model-v1.0.0-symbol.json new file mode 100644 index 000000000..47354b642 --- /dev/null +++ b/cnocr_models/cnocr/models/model-v1.0.0-symbol.json @@ -0,0 +1,1354 @@ +{ + "nodes": [ + { + "op": "null", + "name": "data", + "inputs": [] + }, + { + "op": "null", + "name": "conv-0_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-0_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-0", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [[0, 0, 0], [1, 0, 0], [2, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-0_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-0", + "inputs": [[3, 0, 0], [4, 0, 0], [5, 0, 0], [6, 0, 1], [7, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-0", + "inputs": [[8, 0, 0]] + }, + { + "op": "null", + "name": "conv-0-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-0-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-0-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [[9, 0, 0], [10, 0, 0], [11, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-0-1x1", + "inputs": [[12, 0, 0], [13, 0, 0], [14, 0, 0], [15, 0, 1], [16, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-0-1x1", + "inputs": [[17, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-0_m", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[18, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-0_a", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "avg", + "stride": "(2, 2)" + }, + "inputs": [[18, 0, 0]] + }, + { + "op": "elemwise_sub", + "name": "_minus0", + "inputs": [[19, 0, 0], [20, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-1", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[21, 0, 0]] + }, + { + "op": "null", + "name": "conv-2_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-2_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-2", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [[22, 0, 0], [23, 0, 0], [24, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-2_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-2", + "inputs": [[25, 0, 0], [26, 0, 0], [27, 0, 0], [28, 0, 1], [29, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-2", + "inputs": [[30, 0, 0]] + }, + { + "op": "null", + "name": "conv-2-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-2-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-2-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [[31, 0, 0], [32, 0, 0], [33, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-2-1x1", + "inputs": [[34, 0, 0], [35, 0, 0], [36, 0, 0], [37, 0, 1], [38, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-2-1x1", + "inputs": [[39, 0, 0]] + }, + { + "op": "null", + "name": "conv-3_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-3_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-3", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[40, 0, 0], [41, 0, 0], [42, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-3_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-3", + "inputs": [[43, 0, 0], [44, 0, 0], [45, 0, 0], [46, 0, 1], [47, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-3", + "inputs": [[48, 0, 0]] + }, + { + "op": "null", + "name": "conv-3-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-3-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-3-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[49, 0, 0], [50, 0, 0], [51, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-3-1x1", + "inputs": [[52, 0, 0], [53, 0, 0], [54, 0, 0], [55, 0, 1], [56, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-3-1x1", + "inputs": [[57, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-2", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[58, 0, 0]] + }, + { + "op": "null", + "name": "conv-4_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-4_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-4", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[59, 0, 0], [60, 0, 0], [61, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-4_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-4", + "inputs": [[62, 0, 0], [63, 0, 0], [64, 0, 0], [65, 0, 1], [66, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-4", + "inputs": [[67, 0, 0]] + }, + { + "op": "null", + "name": "conv-4-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-4-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-4-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[68, 0, 0], [69, 0, 0], [70, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-4-1x1", + "inputs": [[71, 0, 0], [72, 0, 0], [73, 0, 0], [74, 0, 1], [75, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-4-1x1", + "inputs": [[76, 0, 0]] + }, + { + "op": "null", + "name": "conv-5_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-5_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-5", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[77, 0, 0], [78, 0, 0], [79, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-5_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-5", + "inputs": [[80, 0, 0], [81, 0, 0], [82, 0, 0], [83, 0, 1], [84, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-5", + "inputs": [[85, 0, 0]] + }, + { + "op": "null", + "name": "conv-5-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-5-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-5-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[86, 0, 0], [87, 0, 0], [88, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-5-1x1", + "inputs": [[89, 0, 0], [90, 0, 0], [91, 0, 0], [92, 0, 1], [93, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-5-1x1", + "inputs": [[94, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool1", + "attrs": { + "kernel": "(4, 1)", + "pool_type": "avg" + }, + "inputs": [[95, 0, 0]] + }, + { + "op": "Dropout", + "name": "dropout0", + "attrs": {"p": "0.5"}, + "inputs": [[96, 0, 0]] + }, + { + "op": "squeeze", + "name": "squeeze0", + "attrs": {"axis": "2"}, + "inputs": [[97, 0, 0]] + }, + { + "op": "transpose", + "name": "transpose0", + "attrs": {"axes": "(2, 0, 1)"}, + "inputs": [[98, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 0)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape0", + "attrs": {"shape": "-1"}, + "inputs": [[100, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape1", + "attrs": {"shape": "-1"}, + "inputs": [[102, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 0)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape2", + "attrs": {"shape": "-1"}, + "inputs": [[104, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape3", + "attrs": {"shape": "-1"}, + "inputs": [[106, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 200)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape4", + "attrs": {"shape": "-1"}, + "inputs": [[108, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape5", + "attrs": {"shape": "-1"}, + "inputs": [[110, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 200)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape6", + "attrs": {"shape": "-1"}, + "inputs": [[112, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape7", + "attrs": {"shape": "-1"}, + "inputs": [[114, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape8", + "attrs": {"shape": "-1"}, + "inputs": [[116, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape9", + "attrs": {"shape": "-1"}, + "inputs": [[118, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape10", + "attrs": {"shape": "-1"}, + "inputs": [[120, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape11", + "attrs": {"shape": "-1"}, + "inputs": [[122, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape12", + "attrs": {"shape": "-1"}, + "inputs": [[124, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape13", + "attrs": {"shape": "-1"}, + "inputs": [[126, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape14", + "attrs": {"shape": "-1"}, + "inputs": [[128, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape15", + "attrs": {"shape": "-1"}, + "inputs": [[130, 0, 0]] + }, + { + "op": "_rnn_param_concat", + "name": "lstm0__rnn_param_concat0", + "attrs": { + "dim": "0", + "num_args": "16" + }, + "inputs": [ + [101, 0, 0], + [103, 0, 0], + [105, 0, 0], + [107, 0, 0], + [109, 0, 0], + [111, 0, 0], + [113, 0, 0], + [115, 0, 0], + [117, 0, 0], + [119, 0, 0], + [121, 0, 0], + [123, 0, 0], + [125, 0, 0], + [127, 0, 0], + [129, 0, 0], + [131, 0, 0] + ] + }, + { + "op": "_zeros", + "name": "lstm0_lstm0_h0_0", + "attrs": { + "__layout__": "LNC", + "dtype": "float32", + "shape": "(4, 0, 100)" + }, + "inputs": [] + }, + { + "op": "_zeros", + "name": "lstm0_lstm0_h0_1", + "attrs": { + "__layout__": "LNC", + "dtype": "float32", + "shape": "(4, 0, 100)" + }, + "inputs": [] + }, + { + "op": "RNN", + "name": "lstm0_rnn0", + "attrs": { + "bidirectional": "True", + "lstm_state_clip_max": "None", + "lstm_state_clip_min": "None", + "lstm_state_clip_nan": "False", + "mode": "lstm", + "num_layers": "2", + "p": "0", + "projection_size": "None", + "state_outputs": "True", + "state_size": "100" + }, + "inputs": [[99, 0, 0], [132, 0, 0], [133, 0, 0], [134, 0, 0]] + }, + { + "op": "Reshape", + "name": "reshape0", + "attrs": {"shape": "(-3, -2)"}, + "inputs": [[135, 0, 0]] + }, + { + "op": "null", + "name": "pred_fc_weight", + "attrs": {"num_hidden": "6426"}, + "inputs": [] + }, + { + "op": "null", + "name": "pred_fc_bias", + "attrs": {"num_hidden": "6426"}, + "inputs": [] + }, + { + "op": "FullyConnected", + "name": "pred_fc", + "attrs": {"num_hidden": "6426"}, + "inputs": [[136, 0, 0], [137, 0, 0], [138, 0, 0]] + }, + { + "op": "SoftmaxActivation", + "name": "softmaxactivation0", + "inputs": [[139, 0, 0]] + }, + { + "op": "MakeLoss", + "name": "makeloss1", + "inputs": [[140, 0, 0]] + }, + { + "op": "BlockGrad", + "name": "blockgrad0", + "inputs": [[141, 0, 0]] + }, + { + "op": "Reshape", + "name": "reshape1", + "attrs": {"shape": "(-4, 35, -1, 0)"}, + "inputs": [[139, 0, 0]] + }, + { + "op": "null", + "name": "label", + "inputs": [] + }, + { + "op": "CTCLoss", + "name": "ctc_loss0", + "inputs": [[143, 0, 0], [144, 0, 0]] + }, + { + "op": "MakeLoss", + "name": "makeloss0", + "inputs": [[145, 0, 0]] + } + ], + "arg_nodes": [ + 0, + 1, + 2, + 4, + 5, + 6, + 7, + 10, + 11, + 13, + 14, + 15, + 16, + 23, + 24, + 26, + 27, + 28, + 29, + 32, + 33, + 35, + 36, + 37, + 38, + 41, + 42, + 44, + 45, + 46, + 47, + 50, + 51, + 53, + 54, + 55, + 56, + 60, + 61, + 63, + 64, + 65, + 66, + 69, + 70, + 72, + 73, + 74, + 75, + 78, + 79, + 81, + 82, + 83, + 84, + 87, + 88, + 90, + 91, + 92, + 93, + 100, + 102, + 104, + 106, + 108, + 110, + 112, + 114, + 116, + 118, + 120, + 122, + 124, + 126, + 128, + 130, + 137, + 138, + 144 + ], + "node_row_ptr": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 115, + 116, + 117, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 170, + 171 + ], + "heads": [[142, 0, 0], [146, 0, 0]], + "attrs": {"mxnet_version": ["int", 10401]} +} \ No newline at end of file diff --git a/cnocr_models/digit/models/label_cn.txt b/cnocr_models/digit/models/label_cn.txt new file mode 100644 index 000000000..2fd8f59b2 --- /dev/null +++ b/cnocr_models/digit/models/label_cn.txt @@ -0,0 +1,11 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 + \ No newline at end of file diff --git a/cnocr_models/digit/models/model-v1.0.0-0060.params b/cnocr_models/digit/models/model-v1.0.0-0060.params new file mode 100644 index 000000000..645284db0 Binary files /dev/null and b/cnocr_models/digit/models/model-v1.0.0-0060.params differ diff --git a/cnocr_models/digit/models/model-v1.0.0-symbol.json b/cnocr_models/digit/models/model-v1.0.0-symbol.json new file mode 100644 index 000000000..a55536e69 --- /dev/null +++ b/cnocr_models/digit/models/model-v1.0.0-symbol.json @@ -0,0 +1,1354 @@ +{ + "nodes": [ + { + "op": "null", + "name": "data", + "inputs": [] + }, + { + "op": "null", + "name": "conv-0_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-0_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-0", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [[0, 0, 0], [1, 0, 0], [2, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-0_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-0", + "inputs": [[3, 0, 0], [4, 0, 0], [5, 0, 0], [6, 0, 1], [7, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-0", + "inputs": [[8, 0, 0]] + }, + { + "op": "null", + "name": "conv-0-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-0-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-0-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [[9, 0, 0], [10, 0, 0], [11, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-0-1x1", + "inputs": [[12, 0, 0], [13, 0, 0], [14, 0, 0], [15, 0, 1], [16, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-0-1x1", + "inputs": [[17, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-0_m", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[18, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-0_a", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "avg", + "stride": "(2, 2)" + }, + "inputs": [[18, 0, 0]] + }, + { + "op": "elemwise_sub", + "name": "_minus0", + "inputs": [[19, 0, 0], [20, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-1", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[21, 0, 0]] + }, + { + "op": "null", + "name": "conv-2_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-2_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-2", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [[22, 0, 0], [23, 0, 0], [24, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-2_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-2", + "inputs": [[25, 0, 0], [26, 0, 0], [27, 0, 0], [28, 0, 1], [29, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-2", + "inputs": [[30, 0, 0]] + }, + { + "op": "null", + "name": "conv-2-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-2-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-2-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [[31, 0, 0], [32, 0, 0], [33, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-2-1x1", + "inputs": [[34, 0, 0], [35, 0, 0], [36, 0, 0], [37, 0, 1], [38, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-2-1x1", + "inputs": [[39, 0, 0]] + }, + { + "op": "null", + "name": "conv-3_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-3_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-3", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[40, 0, 0], [41, 0, 0], [42, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-3_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-3", + "inputs": [[43, 0, 0], [44, 0, 0], [45, 0, 0], [46, 0, 1], [47, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-3", + "inputs": [[48, 0, 0]] + }, + { + "op": "null", + "name": "conv-3-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-3-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-3-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[49, 0, 0], [50, 0, 0], [51, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-3-1x1", + "inputs": [[52, 0, 0], [53, 0, 0], [54, 0, 0], [55, 0, 1], [56, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-3-1x1", + "inputs": [[57, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-2", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[58, 0, 0]] + }, + { + "op": "null", + "name": "conv-4_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-4_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-4", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[59, 0, 0], [60, 0, 0], [61, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-4_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-4", + "inputs": [[62, 0, 0], [63, 0, 0], [64, 0, 0], [65, 0, 1], [66, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-4", + "inputs": [[67, 0, 0]] + }, + { + "op": "null", + "name": "conv-4-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-4-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-4-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[68, 0, 0], [69, 0, 0], [70, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-4-1x1", + "inputs": [[71, 0, 0], [72, 0, 0], [73, 0, 0], [74, 0, 1], [75, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-4-1x1", + "inputs": [[76, 0, 0]] + }, + { + "op": "null", + "name": "conv-5_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-5_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-5", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[77, 0, 0], [78, 0, 0], [79, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-5_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-5", + "inputs": [[80, 0, 0], [81, 0, 0], [82, 0, 0], [83, 0, 1], [84, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-5", + "inputs": [[85, 0, 0]] + }, + { + "op": "null", + "name": "conv-5-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-5-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-5-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[86, 0, 0], [87, 0, 0], [88, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-5-1x1", + "inputs": [[89, 0, 0], [90, 0, 0], [91, 0, 0], [92, 0, 1], [93, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-5-1x1", + "inputs": [[94, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool1", + "attrs": { + "kernel": "(4, 1)", + "pool_type": "avg" + }, + "inputs": [[95, 0, 0]] + }, + { + "op": "Dropout", + "name": "dropout0", + "attrs": {"p": "0.5"}, + "inputs": [[96, 0, 0]] + }, + { + "op": "squeeze", + "name": "squeeze0", + "attrs": {"axis": "2"}, + "inputs": [[97, 0, 0]] + }, + { + "op": "transpose", + "name": "transpose0", + "attrs": {"axes": "(2, 0, 1)"}, + "inputs": [[98, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 0)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape0", + "attrs": {"shape": "-1"}, + "inputs": [[100, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape1", + "attrs": {"shape": "-1"}, + "inputs": [[102, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 0)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape2", + "attrs": {"shape": "-1"}, + "inputs": [[104, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape3", + "attrs": {"shape": "-1"}, + "inputs": [[106, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 200)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape4", + "attrs": {"shape": "-1"}, + "inputs": [[108, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape5", + "attrs": {"shape": "-1"}, + "inputs": [[110, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 200)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape6", + "attrs": {"shape": "-1"}, + "inputs": [[112, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape7", + "attrs": {"shape": "-1"}, + "inputs": [[114, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape8", + "attrs": {"shape": "-1"}, + "inputs": [[116, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape9", + "attrs": {"shape": "-1"}, + "inputs": [[118, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape10", + "attrs": {"shape": "-1"}, + "inputs": [[120, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape11", + "attrs": {"shape": "-1"}, + "inputs": [[122, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape12", + "attrs": {"shape": "-1"}, + "inputs": [[124, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape13", + "attrs": {"shape": "-1"}, + "inputs": [[126, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape14", + "attrs": {"shape": "-1"}, + "inputs": [[128, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape15", + "attrs": {"shape": "-1"}, + "inputs": [[130, 0, 0]] + }, + { + "op": "_rnn_param_concat", + "name": "lstm0__rnn_param_concat0", + "attrs": { + "dim": "0", + "num_args": "16" + }, + "inputs": [ + [101, 0, 0], + [103, 0, 0], + [105, 0, 0], + [107, 0, 0], + [109, 0, 0], + [111, 0, 0], + [113, 0, 0], + [115, 0, 0], + [117, 0, 0], + [119, 0, 0], + [121, 0, 0], + [123, 0, 0], + [125, 0, 0], + [127, 0, 0], + [129, 0, 0], + [131, 0, 0] + ] + }, + { + "op": "_zeros", + "name": "lstm0_lstm0_h0_0", + "attrs": { + "__layout__": "LNC", + "dtype": "float32", + "shape": "(4, 0, 100)" + }, + "inputs": [] + }, + { + "op": "_zeros", + "name": "lstm0_lstm0_h0_1", + "attrs": { + "__layout__": "LNC", + "dtype": "float32", + "shape": "(4, 0, 100)" + }, + "inputs": [] + }, + { + "op": "RNN", + "name": "lstm0_rnn0", + "attrs": { + "bidirectional": "True", + "lstm_state_clip_max": "None", + "lstm_state_clip_min": "None", + "lstm_state_clip_nan": "False", + "mode": "lstm", + "num_layers": "2", + "p": "0", + "projection_size": "None", + "state_outputs": "True", + "state_size": "100" + }, + "inputs": [[99, 0, 0], [132, 0, 0], [133, 0, 0], [134, 0, 0]] + }, + { + "op": "Reshape", + "name": "reshape0", + "attrs": {"shape": "(-3, -2)"}, + "inputs": [[135, 0, 0]] + }, + { + "op": "null", + "name": "pred_fc_weight", + "attrs": {"num_hidden": "11"}, + "inputs": [] + }, + { + "op": "null", + "name": "pred_fc_bias", + "attrs": {"num_hidden": "11"}, + "inputs": [] + }, + { + "op": "FullyConnected", + "name": "pred_fc", + "attrs": {"num_hidden": "11"}, + "inputs": [[136, 0, 0], [137, 0, 0], [138, 0, 0]] + }, + { + "op": "SoftmaxActivation", + "name": "softmaxactivation0", + "inputs": [[139, 0, 0]] + }, + { + "op": "MakeLoss", + "name": "makeloss1", + "inputs": [[140, 0, 0]] + }, + { + "op": "BlockGrad", + "name": "blockgrad0", + "inputs": [[141, 0, 0]] + }, + { + "op": "Reshape", + "name": "reshape1", + "attrs": {"shape": "(-4, 35, -1, 0)"}, + "inputs": [[139, 0, 0]] + }, + { + "op": "null", + "name": "label", + "inputs": [] + }, + { + "op": "CTCLoss", + "name": "ctc_loss0", + "inputs": [[143, 0, 0], [144, 0, 0]] + }, + { + "op": "MakeLoss", + "name": "makeloss0", + "inputs": [[145, 0, 0]] + } + ], + "arg_nodes": [ + 0, + 1, + 2, + 4, + 5, + 6, + 7, + 10, + 11, + 13, + 14, + 15, + 16, + 23, + 24, + 26, + 27, + 28, + 29, + 32, + 33, + 35, + 36, + 37, + 38, + 41, + 42, + 44, + 45, + 46, + 47, + 50, + 51, + 53, + 54, + 55, + 56, + 60, + 61, + 63, + 64, + 65, + 66, + 69, + 70, + 72, + 73, + 74, + 75, + 78, + 79, + 81, + 82, + 83, + 84, + 87, + 88, + 90, + 91, + 92, + 93, + 100, + 102, + 104, + 106, + 108, + 110, + 112, + 114, + 116, + 118, + 120, + 122, + 124, + 126, + 128, + 130, + 137, + 138, + 144 + ], + "node_row_ptr": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 115, + 116, + 117, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 170, + 171 + ], + "heads": [[142, 0, 0], [146, 0, 0]], + "attrs": {"mxnet_version": ["int", 10401]} +} \ No newline at end of file diff --git a/cnocr_models/stage/models/label_cn.txt b/cnocr_models/stage/models/label_cn.txt new file mode 100644 index 000000000..78c9de722 --- /dev/null +++ b/cnocr_models/stage/models/label_cn.txt @@ -0,0 +1,22 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F +S +P +- +: +/ + \ No newline at end of file diff --git a/cnocr_models/stage/models/model-v1.0.0-0056.params b/cnocr_models/stage/models/model-v1.0.0-0056.params new file mode 100644 index 000000000..a4a0791c6 Binary files /dev/null and b/cnocr_models/stage/models/model-v1.0.0-0056.params differ diff --git a/cnocr_models/stage/models/model-v1.0.0-symbol.json b/cnocr_models/stage/models/model-v1.0.0-symbol.json new file mode 100644 index 000000000..e383701f9 --- /dev/null +++ b/cnocr_models/stage/models/model-v1.0.0-symbol.json @@ -0,0 +1,1354 @@ +{ + "nodes": [ + { + "op": "null", + "name": "data", + "inputs": [] + }, + { + "op": "null", + "name": "conv-0_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-0_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-0", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "64", + "pad": "(1, 1)" + }, + "inputs": [[0, 0, 0], [1, 0, 0], [2, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-0_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-0", + "inputs": [[3, 0, 0], [4, 0, 0], [5, 0, 0], [6, 0, 1], [7, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-0", + "inputs": [[8, 0, 0]] + }, + { + "op": "null", + "name": "conv-0-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-0-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-0-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "64", + "pad": "(0, 0)" + }, + "inputs": [[9, 0, 0], [10, 0, 0], [11, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-0-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-0-1x1", + "inputs": [[12, 0, 0], [13, 0, 0], [14, 0, 0], [15, 0, 1], [16, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-0-1x1", + "inputs": [[17, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-0_m", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[18, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-0_a", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "avg", + "stride": "(2, 2)" + }, + "inputs": [[18, 0, 0]] + }, + { + "op": "elemwise_sub", + "name": "_minus0", + "inputs": [[19, 0, 0], [20, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-1", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[21, 0, 0]] + }, + { + "op": "null", + "name": "conv-2_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-2_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-2", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "256", + "pad": "(1, 1)" + }, + "inputs": [[22, 0, 0], [23, 0, 0], [24, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-2_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-2", + "inputs": [[25, 0, 0], [26, 0, 0], [27, 0, 0], [28, 0, 1], [29, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-2", + "inputs": [[30, 0, 0]] + }, + { + "op": "null", + "name": "conv-2-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-2-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-2-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "256", + "pad": "(0, 0)" + }, + "inputs": [[31, 0, 0], [32, 0, 0], [33, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-2-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-2-1x1", + "inputs": [[34, 0, 0], [35, 0, 0], [36, 0, 0], [37, 0, 1], [38, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-2-1x1", + "inputs": [[39, 0, 0]] + }, + { + "op": "null", + "name": "conv-3_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-3_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-3", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[40, 0, 0], [41, 0, 0], [42, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-3_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-3", + "inputs": [[43, 0, 0], [44, 0, 0], [45, 0, 0], [46, 0, 1], [47, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-3", + "inputs": [[48, 0, 0]] + }, + { + "op": "null", + "name": "conv-3-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-3-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-3-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[49, 0, 0], [50, 0, 0], [51, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-3-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-3-1x1", + "inputs": [[52, 0, 0], [53, 0, 0], [54, 0, 0], [55, 0, 1], [56, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-3-1x1", + "inputs": [[57, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool-2", + "attrs": { + "kernel": "(2, 2)", + "pool_type": "max", + "stride": "(2, 2)" + }, + "inputs": [[58, 0, 0]] + }, + { + "op": "null", + "name": "conv-4_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-4_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-4", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[59, 0, 0], [60, 0, 0], [61, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-4_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-4", + "inputs": [[62, 0, 0], [63, 0, 0], [64, 0, 0], [65, 0, 1], [66, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-4", + "inputs": [[67, 0, 0]] + }, + { + "op": "null", + "name": "conv-4-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-4-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-4-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[68, 0, 0], [69, 0, 0], [70, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-4-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-4-1x1", + "inputs": [[71, 0, 0], [72, 0, 0], [73, 0, 0], [74, 0, 1], [75, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-4-1x1", + "inputs": [[76, 0, 0]] + }, + { + "op": "null", + "name": "conv-5_weight", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-5_bias", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-5", + "attrs": { + "kernel": "(3, 3)", + "num_filter": "512", + "pad": "(1, 1)" + }, + "inputs": [[77, 0, 0], [78, 0, 0], [79, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-5_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-5", + "inputs": [[80, 0, 0], [81, 0, 0], [82, 0, 0], [83, 0, 1], [84, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-5", + "inputs": [[85, 0, 0]] + }, + { + "op": "null", + "name": "conv-5-1x1_weight", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "null", + "name": "conv-5-1x1_bias", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [] + }, + { + "op": "Convolution", + "name": "conv-5-1x1", + "attrs": { + "kernel": "(1, 1)", + "num_filter": "512", + "pad": "(0, 0)" + }, + "inputs": [[86, 0, 0], [87, 0, 0], [88, 0, 0]] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_gamma", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_beta", + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_moving_mean", + "attrs": {"__init__": "[\"zero\", {}]"}, + "inputs": [] + }, + { + "op": "null", + "name": "batchnorm-5-1x1_moving_var", + "attrs": {"__init__": "[\"one\", {}]"}, + "inputs": [] + }, + { + "op": "BatchNorm", + "name": "batchnorm-5-1x1", + "inputs": [[89, 0, 0], [90, 0, 0], [91, 0, 0], [92, 0, 1], [93, 0, 1]] + }, + { + "op": "LeakyReLU", + "name": "leakyrelu-5-1x1", + "inputs": [[94, 0, 0]] + }, + { + "op": "Pooling", + "name": "pool1", + "attrs": { + "kernel": "(4, 1)", + "pool_type": "avg" + }, + "inputs": [[95, 0, 0]] + }, + { + "op": "Dropout", + "name": "dropout0", + "attrs": {"p": "0.5"}, + "inputs": [[96, 0, 0]] + }, + { + "op": "squeeze", + "name": "squeeze0", + "attrs": {"axis": "2"}, + "inputs": [[97, 0, 0]] + }, + { + "op": "transpose", + "name": "transpose0", + "attrs": {"axes": "(2, 0, 1)"}, + "inputs": [[98, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 0)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape0", + "attrs": {"shape": "-1"}, + "inputs": [[100, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape1", + "attrs": {"shape": "-1"}, + "inputs": [[102, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 0)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape2", + "attrs": {"shape": "-1"}, + "inputs": [[104, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape3", + "attrs": {"shape": "-1"}, + "inputs": [[106, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 200)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape4", + "attrs": {"shape": "-1"}, + "inputs": [[108, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape5", + "attrs": {"shape": "-1"}, + "inputs": [[110, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_i2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 200)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape6", + "attrs": {"shape": "-1"}, + "inputs": [[112, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_h2h_weight", + "attrs": { + "__dtype__": "0", + "__lr_mult__": "1.0", + "__shape__": "(400, 100)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape7", + "attrs": {"shape": "-1"}, + "inputs": [[114, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape8", + "attrs": {"shape": "-1"}, + "inputs": [[116, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l0_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape9", + "attrs": {"shape": "-1"}, + "inputs": [[118, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape10", + "attrs": {"shape": "-1"}, + "inputs": [[120, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r0_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape11", + "attrs": {"shape": "-1"}, + "inputs": [[122, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape12", + "attrs": {"shape": "-1"}, + "inputs": [[124, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_l1_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape13", + "attrs": {"shape": "-1"}, + "inputs": [[126, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_i2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape14", + "attrs": {"shape": "-1"}, + "inputs": [[128, 0, 0]] + }, + { + "op": "null", + "name": "lstm0_r1_h2h_bias", + "attrs": { + "__dtype__": "0", + "__init__": "zeros", + "__lr_mult__": "1.0", + "__shape__": "(400,)", + "__storage_type__": "0", + "__wd_mult__": "1.0" + }, + "inputs": [] + }, + { + "op": "Reshape", + "name": "lstm0_reshape15", + "attrs": {"shape": "-1"}, + "inputs": [[130, 0, 0]] + }, + { + "op": "_rnn_param_concat", + "name": "lstm0__rnn_param_concat0", + "attrs": { + "dim": "0", + "num_args": "16" + }, + "inputs": [ + [101, 0, 0], + [103, 0, 0], + [105, 0, 0], + [107, 0, 0], + [109, 0, 0], + [111, 0, 0], + [113, 0, 0], + [115, 0, 0], + [117, 0, 0], + [119, 0, 0], + [121, 0, 0], + [123, 0, 0], + [125, 0, 0], + [127, 0, 0], + [129, 0, 0], + [131, 0, 0] + ] + }, + { + "op": "_zeros", + "name": "lstm0_lstm0_h0_0", + "attrs": { + "__layout__": "LNC", + "dtype": "float32", + "shape": "(4, 0, 100)" + }, + "inputs": [] + }, + { + "op": "_zeros", + "name": "lstm0_lstm0_h0_1", + "attrs": { + "__layout__": "LNC", + "dtype": "float32", + "shape": "(4, 0, 100)" + }, + "inputs": [] + }, + { + "op": "RNN", + "name": "lstm0_rnn0", + "attrs": { + "bidirectional": "True", + "lstm_state_clip_max": "None", + "lstm_state_clip_min": "None", + "lstm_state_clip_nan": "False", + "mode": "lstm", + "num_layers": "2", + "p": "0", + "projection_size": "None", + "state_outputs": "True", + "state_size": "100" + }, + "inputs": [[99, 0, 0], [132, 0, 0], [133, 0, 0], [134, 0, 0]] + }, + { + "op": "Reshape", + "name": "reshape0", + "attrs": {"shape": "(-3, -2)"}, + "inputs": [[135, 0, 0]] + }, + { + "op": "null", + "name": "pred_fc_weight", + "attrs": {"num_hidden": "22"}, + "inputs": [] + }, + { + "op": "null", + "name": "pred_fc_bias", + "attrs": {"num_hidden": "22"}, + "inputs": [] + }, + { + "op": "FullyConnected", + "name": "pred_fc", + "attrs": {"num_hidden": "22"}, + "inputs": [[136, 0, 0], [137, 0, 0], [138, 0, 0]] + }, + { + "op": "SoftmaxActivation", + "name": "softmaxactivation0", + "inputs": [[139, 0, 0]] + }, + { + "op": "MakeLoss", + "name": "makeloss1", + "inputs": [[140, 0, 0]] + }, + { + "op": "BlockGrad", + "name": "blockgrad0", + "inputs": [[141, 0, 0]] + }, + { + "op": "Reshape", + "name": "reshape1", + "attrs": {"shape": "(-4, 35, -1, 0)"}, + "inputs": [[139, 0, 0]] + }, + { + "op": "null", + "name": "label", + "inputs": [] + }, + { + "op": "CTCLoss", + "name": "ctc_loss0", + "inputs": [[143, 0, 0], [144, 0, 0]] + }, + { + "op": "MakeLoss", + "name": "makeloss0", + "inputs": [[145, 0, 0]] + } + ], + "arg_nodes": [ + 0, + 1, + 2, + 4, + 5, + 6, + 7, + 10, + 11, + 13, + 14, + 15, + 16, + 23, + 24, + 26, + 27, + 28, + 29, + 32, + 33, + 35, + 36, + 37, + 38, + 41, + 42, + 44, + 45, + 46, + 47, + 50, + 51, + 53, + 54, + 55, + 56, + 60, + 61, + 63, + 64, + 65, + 66, + 69, + 70, + 72, + 73, + 74, + 75, + 78, + 79, + 81, + 82, + 83, + 84, + 87, + 88, + 90, + 91, + 92, + 93, + 100, + 102, + 104, + 106, + 108, + 110, + 112, + 114, + 116, + 118, + 120, + 122, + 124, + 126, + 128, + 130, + 137, + 138, + 144 + ], + "node_row_ptr": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 115, + 116, + 117, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 170, + 171 + ], + "heads": [[142, 0, 0], [146, 0, 0]], + "attrs": {"mxnet_version": ["int", 10401]} +} \ No newline at end of file diff --git a/config/main_template.conf b/config/main_template.conf new file mode 100644 index 000000000..382f62465 --- /dev/null +++ b/config/main_template.conf @@ -0,0 +1,110 @@ +[Command] +command = Daily + +[Setting] +enable_stop_condition = yes +if_count_greater_than = 0 +if_time_reach = 0 +if_oil_lower_than = 2000 +if_trigger_emotion_control = yes +if_dock_full = no +enable_fleet_control = yes +fleet_index_1 = 1 +fleet_formation_1 = formation_2 +fleet_index_2 = 2 +fleet_formation_2 = formation_2 +fleet_index_3 = 1 +fleet_formation_3 = formation_2 +fleet_index_4 = do_not_use +submarine_mode = do_not_use +enable_emotion_reduce = yes +ignore_low_emotion_warn = no +emotion_recover_1 = dormitory_floor_2 +emotion_control_1 = keep_high_emotion +hole_fleet_married_1 = no +emotion_recover_2 = not_in_dormitory +emotion_control_2 = avoid_yellow_face +hole_fleet_married_2 = no +emotion_recover_3 = dormitory_floor_1 +emotion_control_3 = keep_high_emotion +hole_fleet_married_3 = no +enable_hp_balance = no +enable_retirement = yes +retire_mode = retire_10 +retire_n = yes +retire_r = no +retire_sr = no +retire_ssr = no +enable_drop_screenshot = no +drop_screenshot_folder = +serial = 127.0.0.1:62001 +command = setting + +[Daily] +enable_daily_mission = yes +enable_hard_campaign = yes +enable_exercise = yes +daily_mission_1 = daily_gun +daily_mission_2 = lv_70 +daily_mission_4 = lv_70 +daily_mission_5 = lv_70 +daily_fleet = 5 +daily_equipment = 0 +hard_campaign = 10-4 +hard_fleet = 1 +hard_equipment = 0 +exercise_choose_mode = max_exp +exercise_preserve = 0 +exercise_try = 1 +exercise_hp_threshold = 0.40 +exercise_low_hp_confirm = 1.0 +exercise_equipment = 0 +command = daily + +[Main] +command = main +main_stage = 7-2 + +[Event] +command = event +event_stage = event_20200312cn_sp3 + +[Event_daily_ab] +event_name_ab = event_20200326_cn +command = event_daily_ab + +[Semi_auto] +command = semi_auto +enable_semi_map_preparation = no +enable_semi_story_skip = yes + +[C72_mystery_farming] +command = c72_mystery_farming + +[C124_leveling] +non_s3_enemy_enter_tolerance = 1 +non_s3_enemy_withdraw_tolerance = 0 +ammo_pick_up_124 = 3 +command = c124_leveling + +[EmotionRecord] +fleet_1_emotion = 150 +fleet_1_savetime = 2020-01-01_00:00:00 +fleet_2_emotion = 119 +fleet_2_savetime = 2020-01-01_00:00:00 +fleet_3_emotion = 150 +fleet_3_savetime = 2020-01-01_00:00:00 + +[DailyRecord] +daily = 2020-01-01_00:00:00 +hard = 2020-01-01_00:00:00 +exercise = 2020-01-01_00:00:00 + +[EventABRecord] +a1 = 2020-01-01_00:00:00 +a2 = 2020-01-01_00:00:00 +a3 = 2020-01-01_00:00:00 +b1 = 2020-01-01_00:00:00 +b2 = 2020-01-01_00:00:00 +b3 = 2020-01-01_00:00:00 + diff --git a/dev_tools/button_extract.py b/dev_tools/button_extract.py new file mode 100644 index 000000000..2180c41c5 --- /dev/null +++ b/dev_tools/button_extract.py @@ -0,0 +1,171 @@ +import os + +import numpy as np +from PIL import Image + +os.chdir('../') + +from module.base.button import get_color +from module.config.config import AzurLaneConfig +from module.logger import logger + +MODULE_FOLDER = './module' +BUTTON_FILE = 'assets.py' +IMPORT_EXP = """ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. +""" +IMPORT_EXP = IMPORT_EXP.strip().split('\n') + [''] + + +class ImageExtractor: + def __init__(self, module, file, config): + """ + Args: + module(str): + file(str): xxx.png + config(AzurLaneConfig): + """ + self.module = module + self.name = os.path.splitext(file)[0] + self.config = config + self.area, self.color, self.button = (), (), () + self.load() + + def file(self, genre=''): + file = '%s.%s.png' % (self.name, genre) if genre else '%s.png' % self.name + file = os.path.join(self.config.ASSETS_FOLDER, self.module, file) + return file + + @staticmethod + def extract(file): + image = Image.open(file).convert('RGB') + bbox = image.getbbox() + mean = get_color(image=image, area=bbox) + mean = tuple(np.rint(mean).astype(int)) + return bbox, mean + + def load(self): + if os.path.exists(self.file()): + self.area, self.color = self.extract(self.file()) + self.button = self.area + if os.path.exists(self.file('AREA')): + self.area, _ = self.extract(self.file('AREA')) + if os.path.exists(self.file('COLOR')): + _, self.color = self.extract(self.file('COLOR')) + if os.path.exists(self.file('BUTTON')): + self.button, _ = self.extract(self.file('BUTTON')) + + @property + def expression(self): + return '%s = Button(area=%s, color=%s, button=%s, file=\'%s\')' % ( + self.name, self.area, self.color, self.button, + self.config.ASSETS_FOLDER + '/' + self.module + '/' + self.name + '.png') + + +class TemplateExtractor(ImageExtractor): + # def __init__(self, module, file, config): + # """ + # Args: + # module(str): + # file(str): xxx.png + # config(AzurLaneConfig): + # """ + # self.module = module + # self.file = file + # self.config = config + + @property + def expression(self): + # return '%s = Template(file=\'%s\')' % ( + # os.path.splitext(self.file)[0], + # self.config.ASSETS_FOLDER + '/' + self.module + '/' + self.file) + return '%s = Template(area=%s, color=%s, button=%s, file=\'%s\')' % ( + self.name, self.area, self.color, self.button, + self.config.ASSETS_FOLDER + '/' + self.module + '/' + self.name + '.png') + + +# class OcrExtractor(ImageExtractor): +# @property +# def expression(self): +# return '%s = OcrArea(area=%s, color=%s, button=%s, file=\'%s\')' % ( +# self.name, self.area, self.color, self.button, +# self.config.ASSETS_FOLDER + '/' + self.module + '/' + self.name + '.png') + + +class ModuleExtractor: + def __init__(self, name, config): + self.name = name + self.config = config + self.folder = os.path.join(self.config.ASSETS_FOLDER, name) + + @staticmethod + def split(file): + name, ext = os.path.splitext(file) + name, sub = os.path.splitext(name) + return name, sub, ext + + def is_base_image(self, file): + _, sub, _ = self.split(file) + return sub == '' + + @property + def expression(self): + exp = [] + for file in os.listdir(self.folder): + if file[0].isdigit(): + continue + if file.startswith('TEMPLATE_'): + exp.append(TemplateExtractor(module=self.name, file=file, config=self.config).expression) + continue + # if file.startswith('OCR_'): + # exp.append(OcrExtractor(module=self.name, file=file, config=self.config).expression) + # continue + if self.is_base_image(file): + exp.append(ImageExtractor(module=self.name, file=file, config=self.config).expression) + continue + + logger.info('Module: %s(%s)' % (self.name, len(exp))) + exp = IMPORT_EXP + exp + return exp + + def write(self): + folder = os.path.join(MODULE_FOLDER, self.name) + if not os.path.exists(folder): + os.mkdir(folder) + with open(os.path.join(folder, BUTTON_FILE), 'w') as f: + for text in self.expression: + f.write(text + '\n') + + +class AssetExtractor: + """ + Extract Asset to asset.py. + All the filename of assets should be in uppercase. + + Asset name starts with digit will be ignore. + E.g. 2020XXXX.png. + Asset name starts with 'TEMPLATE_' will treat as template. + E.g. TEMPLATE_AMBUSH_EVADE_SUCCESS.png + > TEMPLATE_AMBUSH_EVADE_SUCCESS = Template(file='./assets/handler/TEMPLATE_AMBUSH_EVADE_SUCCESS.png') + Asset name starts other will treat as button. + E.g. GET_MISSION.png + > Button(area=(553, 482, 727, 539), color=(93, 142, 203), button=(553, 482, 727, 539), name='GET_MISSION') + Asset name like XXX.AREA.png, XXX.COLOR.png, XXX.BUTTON.png, will overwrite the attribute of XXX.png. + E.g. BATTLE_STATUS_S.BUTTON.png overwrites the attribute 'button' of BATTLE_STATUS_S + Asset name starts with 'OCR_' will be treat as button. + E.g. OCR_EXERCISE_TIMES.png. + """ + + def __init__(self, config): + logger.info('Assets extract') + for module in os.listdir(config.ASSETS_FOLDER): + if os.path.isdir(os.path.join(config.ASSETS_FOLDER, module)): + me = ModuleExtractor(name=module, config=config) + me.write() + + +ae = AssetExtractor(AzurLaneConfig()) diff --git a/dev_tools/emulator_test.py b/dev_tools/emulator_test.py new file mode 100644 index 000000000..7ae72d214 --- /dev/null +++ b/dev_tools/emulator_test.py @@ -0,0 +1,37 @@ +import time + +import numpy as np + +from module.config.config import AzurLaneConfig +from module.device.device import Device + + +class EmulatorChecker(Device): + def stress_test(self): + record = [] + count = 0 + self._screenshot_adb() + while 1: + t0 = time.time() + # self._screenshot_adb() + self._screenshot_uiautomator2() + # self._click_adb(1270, 360) + # self._click_uiautomator2(1270, 360) + + cost = time.time() - t0 + record.append(cost) + count += 1 + print(count, np.round(np.mean(record), 3), np.round(np.std(record), 3)) + + +class Config: + SERIAL = '127.0.0.1:62001' + # SERIAL = '127.0.0.1:7555' + # SERIAL = 'emulator-5554' + # SERIAL = '127.0.0.1:21503' + + USE_ADB_SREENSHOT = False + + +az = EmulatorChecker(AzurLaneConfig().merge(Config())) +az.stress_test() diff --git a/doc/Installation.md b/doc/Installation.md new file mode 100644 index 000000000..acc6c5f8a --- /dev/null +++ b/doc/Installation.md @@ -0,0 +1,82 @@ +# 安装 Installation + +- Clone 本项目 + +- 安装依赖, 最好使用虚拟环境 + + ``` + pip install -r requirements.txt + ``` + +### 安装模拟器 Install a emulator + +| 设备 | Device | 模拟器版本 | 安卓版本 | adb截图 | u2截图 | adb点击 | u2点击 | +|------------|------------|------------|----------|---------|--------|---------|--------| +| 逍遥模拟器 | NemuPlayer | 7.1.3 | 5.1.1 | 0.308 | 0.275 | 0.294 | 0.146 | +| 雷电模拟器 | LDPlayer | 3.83 | 5.1.1 | 0.329 | 0.313 | 0.291 | 0.146 | +| 夜神模拟器 | NoxPlayer | 6.6.0.0 | 5.1.1 | 0.339 | 0.313 | 0.505 | 0.141 | +| MuMu模拟器 | MuMuPlayer | 2.3.1.0 | 6.0.1 | 0.368 | 0.701 | 0.358 | 0.148 | +| 一加5 | Oneplus5 | | 7.1.1 | 1.211 | 0.285 | 0.447 | 0.160 | + +这里给出了一些常见模拟器的性能测试结果, 测试平台 Windows 10, I7-8700k, 1080ti, nvme SSD, 模拟器分辨率1280x720, 碧蓝航线 60帧开启, 进入地图 7-2, 执行100次取平均. + +由于海图识别模块对截图质量有很高的要求, `AzueLaneAutoScript` 暂时不支持手机, 必须使用模拟器. + +- 安装一款安卓模拟器 +- 模拟器分辨率设置为 `1280x720` . + +### 配置ADB Set up ADB + +- 获取 [ADB](https://developer.android.com/studio/releases/platform-tools) + +- 将ADB配置与系统的环境变量中, 并测试是否配置成功. + + ``` + adb devices + ``` + +### 调教国产模拟器 Dealing with chinese emulator + +国产模拟器一般会使用自己的 ADB, 而不同的ADB之间会互相结束对方, 这里提供一个一劳永逸的方法: 直接替换. + +- 前往模拟器的安装目录, 搜索 `adb`, 备份这些文件 +- 将自己的 `adb.exe` 复制进安装目录, 并且把名字改成刚才备份的文件的名字. + +比如说夜神模拟器的安装目录有 `adb.exe` 和 `nox_adb.exe` , 备份它们. 把自己的 `adb.exe` 复制两份进来, 其中一份改名为 `nox_adb.exe` . + +这并不会影响模拟器运行, 还会带来方便. 每次打开模拟器的时候, 模拟器就会自动连接至ADB, 相当于执行了 + +``` +adb connect +``` + +### 安装 uiautomator2 Install uiautomator2 + +[uiautomator2](https://github.com/openatx/uiautomator2), 是一个自动化测试的库, 可以加快截图和点击的速度. `AzueLaneAutoScript` 也可以使用ADB来执行截图和点击, 就是慢一点而已. + +- 执行 + + ``` + python -m uiautomator2 init + ``` + + 会在所有连接的设备上安装 [uiautomator-server](https://github.com/openatx/android-uiautomator-server/releases) , [atx-agent](https://github.com/openatx/atx-agent), [minicap](https://github.com/openstf/minicap), [minitouch](https://github.com/openstf/minitouch) . 如果设备是模拟器, uiautomator2 将跳过 minicap 的安装. + +- 检查 uiautomator2 是否安装成功 + + 修改 `module.dev_tools` 下的 `emulator_test.py` 中的 `SERIAL`, 并执行. + + 一些模拟器的默认 serial: + + | 设备 | Device | serial | + |------------|------------|-----------------| + | 夜神模拟器 | NoxPlayer | 127.0.0.1:62001 | + | MuMu模拟器 | MuMuPlayer | 127.0.0.1:7555 | + | 逍遥模拟器 | NemuPlayer | 127.0.0.1:21503 | + | 雷电模拟器 | LDPlayer | emulator-5554 | + +## 使用方法 Usage + +- 运行 `main.pyw` , 通过图形界面运行 +- 修改配置文件 `config/main.ini` , 在 `main.py` 中调用相关函数 + diff --git a/doc/Perspective.md b/doc/Perspective.md new file mode 100644 index 000000000..e3f57f7c7 --- /dev/null +++ b/doc/Perspective.md @@ -0,0 +1,185 @@ +# 海图识别 + +`海图识别` 是一个碧蓝航线脚本的核心. 如果只是单纯地使用 `模板匹配 (Template matching)` 来进行索敌, 就不可避免地会出现 BOSS被小怪堵住 的情况. `AzurLaneAutoScript` 提供了一个更好的海图识别方法, 在 `module.map` 中, 你将可以得到完整的海域信息, 比如: + +``` +2020-03-10 22:09:03.830 | INFO | A B C D E F G H +2020-03-10 22:09:03.830 | INFO | 1 -- ++ 2E -- -- -- -- -- +2020-03-10 22:09:03.830 | INFO | 2 -- ++ ++ MY -- -- 2E -- +2020-03-10 22:09:03.830 | INFO | 3 == -- FL -- -- -- 2E MY +2020-03-10 22:09:03.830 | INFO | 4 -- == -- -- -- -- ++ ++ +2020-03-10 22:09:03.830 | INFO | 5 -- -- -- 2E -- 2E ++ ++ +``` + +module.map 主要由以下文件构成: + +- perspective.py 透视解析 +- grids.py 海域信息解析 +- camera.py 镜头移动 +- fleet.py 舰队移动 +- map.py 索敌逻辑 + +## 一点透视 + +在理解 `AzurLaneAutoScript` 是如何进行海图识别之前, 需要快速了解一下 `一点透视` 的基本原理. 碧蓝航线的海图是一个一点透视的网格, 解析海图的透视, 需要计算出灭点和距点. + +在一点透视中: + +- 所有的水平线的透视仍为水平线. +- 所有的垂直线相交于一点, 称为 `灭点` . 灭点距离网格越远, 垂直线的透视越接近垂直. + +![vanish_point](perspective.assets/vanish_point.png) + +- 所有的对角线相交于一点, 称为 `距点` . 距点离灭点越远, 网格越扁长. 距点和灭点在同一水平线上. 距点其实有两个, 它们关于灭点对称, 图中画出的是位于灭点左边的距点. + +![distant_point](perspective.assets/distant_point.png) + +## 截图预处理 + +![preprocess](perspective.assets/preprocess.png) + +拿到一张截图之后, `load_image` 函数会进行以下处理 + +- 裁切出用于可以用于识别的区域. +- 去色, 这里使用了 `Photoshop` 里的去色算法, (MAX(R, G, B) + MIN(R, G, B)) // 2 +- 去UI, 这里使用 `overlay.png` . +- 反相 + +(上面的图是反相前的结果, 反相后的图过于恐怖, 就不放了) + +## 网格识别 + +### 网格线识别 + +网格线, 是一条 20% 透明度的黑色线, 在 `720p` 下, 有3至4像素粗. 在旧UI时, 只需要把图像上下左右移动一个像素, 再除以原图像, 便可以得到网格线. 新UI的海图格子增加了白色框, 白色框有透明度渐变, 增加了识别难度. + +`find_peaks` 函数使用了 `scipy.signal.find_peaks` 来寻找网格线. `scipy.signal.find_peaks` 可以寻找数据中的峰值点 : https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html + +截取 height == 370 处图像, 使用以下参数: + +``` +FIND_PEAKS_PARAMETERS = { + 'height': (150, 255 - 40), + 'width': 2, + 'prominence': 10, + 'distance': 35, +} +``` + +![find_peaks](perspective.assets/find_peaks.png) + +可以看出, 有一些被遮挡的没有识别出来, 还有很多识别错误, 不过问题不大. + +然后扫描每一行, 绘制出图像. (出于性能优化, 实际中会把图像展平至一维再识别, 这将缩短时间消耗至约 1/4.) + +![peaks](perspective.assets/peaks.png) + +至此, 我们得到了四幅图像, 分别对应 `垂直的网格内线` `水平的网格内线` `垂直的网格边线` `水平的网格边线` . 这一过程在 `I7-8700k` 上需要花费约 0.13 s, 整个海图识别流程将花费约 0.15 s. + +注意, 识别内线和边线所使用的参数是不一样的. 不同的地图, 应该使用对应的参数, 如果偷懒的话, 也可以使用默认参数, 默认参数是针对 `7-2` 的, 可以在第七章中使用, 甚至可以用到 `北境序曲 D3` . + +## 网格线拟合 + +`hough_lines` 函数使用了 `cv2.HoughLines` 来识别直线, 可以得到四组直线. + +![hough_lines_1](perspective.assets/hough_lines_1.png) + +以 `垂直的网格内线` 为例, 可以看到, 识别结果有一些歪的线. + +我们在图片中间拉一条水平线, 称为 `MID_Y` (如果要修正水平线, 就拉垂直线), 交于垂直线, 交点称为 `mid` , 如果 `mid` 之间的距离小于3, 就认为这些线是相近线, 并用他们的平均值代表他们. 这样就修正了结果. + +## 灭点拟合 + +我们知道, 在一点透视中所有垂直线相交于灭点, 但是网格识别的结果是有误差的, 不能直接求直线的交点. + +`_vanish_point_value` 函数用于计算, 某一点到所有垂直线的距离, 并用 `scipy.optimize.brute` 暴力解出离直线组最近的点, 它就是 `灭点` . 这个曲面描绘了点到垂直线的距离和. 为了在求解是能大胆抛弃距离较远的线, 在求距离是加了 `log` 函数. + +![vanish_point_distance](perspective.assets/vanish_point_distance.png) + +得到灭点后, 还记得之前的 `mid` 吗, 将它们连接至灭点, 作为垂直线. 这是对结果的第二次修正. + +## 距点拟合 + +将最初得到的垂直线和水平线相交, 得到交点. 我们知道距点和灭点在同一水平线上, 在这条水平线上取点, 将所有交点连接至这点, 得到斜线, `_distant_point_value` 函数将计算斜线的 `mid` 之间的距离, 同样使用 `scipy.optimize.brute` 暴力解出距离最小的点, 它就是 `距点` . + +如果将斜线绘制出来, 会有这样的图像, 虽然有很多错误的斜线, 但确实求出了正确的距点. + +![diatant_point_links](perspective.assets/diatant_point_links.png) + +## 网格线清洗 + +经过以上步骤, 我们得到了以下网格线, 大体正确, 但是有错误. + +![mid_cleanse_before](perspective.assets/mid_cleanse_before.png) + +取垂直线的 `mid` , + +``` +[ 185.63733413 315.65944444 441.62998244 446.89313842 573.6301653 + 686.40881027 701.20376316 830.27394123 959.00511191 1087.91874026 + 1220.58809477] +``` + +因为每个格子都是等宽的, 所以 `mid` 理论上是一个等差数列, 但实际识别结果可能有错误的项, 也可能有缺失的项. 我们用一次函数表达这个关系 `y = a * x + b`. 由于错误和缺失, 这里的 `x` 不一定是项数 `n` , 但只要没有10个以上的错误或者缺失, 就会有 `x ∈ [n - 10, n + 10]` . + +接下来, 把表达式改写为 `b = -x * a + y` , 其中 `x ∈ [n - 10, n + 10]` . 如果把`a`当作自变量, 把`b`当作因变量, 那么这是一组直线, 它有 11 * 21 条. 把它们描绘出来: + +![mid_cleanse_lines_with_circle](perspective.assets/mid_cleanse_lines_with_circle.png) + +可以发现, 用橙色圈起来的地方有多条直线重合, 我们称为 `重合点` (`coincident_point`). 那些错误的 `mid` 产生的直线无法与其他直线交于重合点, 自然被剔除. + +使用 `scipy.optimize.brute` 暴力求解所有直线的最近点, 得到`重合点` 的坐标 + +``` +[-201.33197146 129.0958336] +``` + +因此一次函数就是 `y = 129.0958336 * x - 201.33197146` . + +> 在计算点到直线的距离时, 使用了以下函数: +> +> ``` +> distance = 1 / (1 + np.exp(9 / distance) / distance) +> ``` +> 这个函数将削弱距离较远的直线的影响, 鼓励优化器选择局部最优解. +> +> ![mid_cleanse_function](perspective.assets/mid_cleanse_function.png) + +>如何处理水平线? +> +>过`距点`作任意一条直线, 与水平线相交. 将得到的交点与`灭点`连接, 就完成了水平线到垂直线的映射. 处理完再映射回水平线即可. +> +>![mid_cleanse_convert](perspective.assets/mid_cleanse_convert.png) + +最后, 以海图或者屏幕为边界生成 `mid` , 此时缺失的 `mid` 也得到了填充. 重新连接至灭点, 完成了垂直线的清洗. + +绘制出网格识别的结果: + +![mid_cleanse_after](perspective.assets/mid_cleanse_after-1584008112022.png) + +# 网格裁切 + +事实上, 海域中的舰娘, 敌人, 问号等, 都是固定在网格中心的图片, 只不过这些图片会因为透视产生缩放而已. 注意, 仅仅是缩放, 图片不会因为透视产生变形, 产生变形的只有地面的红框和黄框. + +![crop_basic](perspective.assets/crop_basic.png) + +`grid_predictor.py` 中提供了 `get_relative_image` 函数, 它可以根据透视, 裁切出关于网格中心相对位置的图片, 统一缩放到特定大小, 这样就可以愉快地使用模板匹配了. + +``` +from PIL import Image +from module.config.config import cfg +i = Image.open(file) +grids = Grids(i, cfg) +out = Image.new('RGB', tuple((grids.shape + 1) * 105 - 5)) +for loca, grid in grids.grids.items(): + image = grid.get_relative_image( + (-0.415 - 0.7, -0.62 - 0.7, -0.415, -0.62), output_shape=(100, 100)) + out.paste(image, tuple(np.array(loca) * 105)) +out +``` + +![crop_scale](perspective.assets/crop_scale.png) + +## 海域信息解析 + +未完待续 \ No newline at end of file diff --git a/doc/known issue.md b/doc/known issue.md new file mode 100644 index 000000000..4d633716d --- /dev/null +++ b/doc/known issue.md @@ -0,0 +1,24 @@ +# 已知问题 Known issue + +- **无法处理夜间委托的弹窗** 晚上22点会弹出夜间委托(国服21点), 如果在战斗中, 则在战斗后显示, 否则立即弹出. +- **演习可能SL失败** 演习看的是屏幕上方的血槽, 血槽可能被立绘遮挡, 因此需要一定时间(默认1s)血量低于一定值(默认40%)才会触发SL. 一个血皮后排就有30%左右的血槽, 所以别以为在1s内被打掉40%是不可能的. 另外如果后排立绘过大且CD重叠严重, 建议增大确认时间(比如3s), 这样可以减少误判. +- **会显示绿脸黄脸红脸, 红脸出击确认** 这个是瓜游心情值更新不及时的锅了, 只要填对了`RECOVER_PER_HOUR` 和 `EMOTION_LIMIT` 就绝对不会红脸出击, 当然, 也可以保证一直处于经验加成状态. 请相信`module.combat.emotion` +- **有时无法识别舰队图标** +- **不支持潜艇** 潜艇会干扰普通舰队的识别. +- **无法处理网络波动** 重连弹窗, 跳小黄鸡 +- **拖动操作在极少数情况下无效** + + + +# TODO + +- **适配更多地图** +- **支持潜艇** 道中出击, BOSS站出击, BOSS出现后召唤潜艇 +- ~~跳过指挥喵削血动画~~ 已完成 +- **阵型选择** 单纵阵 复纵阵 轮形阵 +- **先锋血量平衡** 把血少的拖到保护位, 这对 低耗队, 练级队很有用. +- **练级队优化** 拣弹药, 主动踩伏击, 低血量撤退 +- **处理夜间委托的弹窗** +- **出击自动站桩** 打低级图的时候, 自律瞎动会超出射程, 站在中间不动打得更快 +- **自动收菜** 委托, 紧急委托, 科研, 后宅喂食, 学技能, 大讲堂, 收小卖部 +- **新地图敌人统计** 不用再等wiki更新了 \ No newline at end of file diff --git a/doc/perspective.assets/crop_basic.png b/doc/perspective.assets/crop_basic.png new file mode 100644 index 000000000..491c879a4 Binary files /dev/null and b/doc/perspective.assets/crop_basic.png differ diff --git a/doc/perspective.assets/crop_enemy.png b/doc/perspective.assets/crop_enemy.png new file mode 100644 index 000000000..699faa2a1 Binary files /dev/null and b/doc/perspective.assets/crop_enemy.png differ diff --git a/doc/perspective.assets/crop_scale.png b/doc/perspective.assets/crop_scale.png new file mode 100644 index 000000000..353c34e60 Binary files /dev/null and b/doc/perspective.assets/crop_scale.png differ diff --git a/doc/perspective.assets/diatant_point_links.png b/doc/perspective.assets/diatant_point_links.png new file mode 100644 index 000000000..22b71b517 Binary files /dev/null and b/doc/perspective.assets/diatant_point_links.png differ diff --git a/doc/perspective.assets/distant_point.png b/doc/perspective.assets/distant_point.png new file mode 100644 index 000000000..f4ec0e87f Binary files /dev/null and b/doc/perspective.assets/distant_point.png differ diff --git a/doc/perspective.assets/find_peaks.png b/doc/perspective.assets/find_peaks.png new file mode 100644 index 000000000..64a8ecaed Binary files /dev/null and b/doc/perspective.assets/find_peaks.png differ diff --git a/doc/perspective.assets/hough_lines_1.png b/doc/perspective.assets/hough_lines_1.png new file mode 100644 index 000000000..5ffe5044b Binary files /dev/null and b/doc/perspective.assets/hough_lines_1.png differ diff --git a/doc/perspective.assets/mid_cleanse_after-1584008112022.png b/doc/perspective.assets/mid_cleanse_after-1584008112022.png new file mode 100644 index 000000000..ca43cbf82 Binary files /dev/null and b/doc/perspective.assets/mid_cleanse_after-1584008112022.png differ diff --git a/doc/perspective.assets/mid_cleanse_after.png b/doc/perspective.assets/mid_cleanse_after.png new file mode 100644 index 000000000..ca43cbf82 Binary files /dev/null and b/doc/perspective.assets/mid_cleanse_after.png differ diff --git a/doc/perspective.assets/mid_cleanse_before.png b/doc/perspective.assets/mid_cleanse_before.png new file mode 100644 index 000000000..f2dfa114d Binary files /dev/null and b/doc/perspective.assets/mid_cleanse_before.png differ diff --git a/doc/perspective.assets/mid_cleanse_convert.png b/doc/perspective.assets/mid_cleanse_convert.png new file mode 100644 index 000000000..166d710e4 Binary files /dev/null and b/doc/perspective.assets/mid_cleanse_convert.png differ diff --git a/doc/perspective.assets/mid_cleanse_function.png b/doc/perspective.assets/mid_cleanse_function.png new file mode 100644 index 000000000..06fcbd252 Binary files /dev/null and b/doc/perspective.assets/mid_cleanse_function.png differ diff --git a/doc/perspective.assets/mid_cleanse_lines_with_circle.png b/doc/perspective.assets/mid_cleanse_lines_with_circle.png new file mode 100644 index 000000000..b6cbb6a42 Binary files /dev/null and b/doc/perspective.assets/mid_cleanse_lines_with_circle.png differ diff --git a/doc/perspective.assets/peaks.png b/doc/perspective.assets/peaks.png new file mode 100644 index 000000000..97e8f5f4b Binary files /dev/null and b/doc/perspective.assets/peaks.png differ diff --git a/doc/perspective.assets/preprocess.png b/doc/perspective.assets/preprocess.png new file mode 100644 index 000000000..125e2210d Binary files /dev/null and b/doc/perspective.assets/preprocess.png differ diff --git a/doc/perspective.assets/vanish_point.png b/doc/perspective.assets/vanish_point.png new file mode 100644 index 000000000..80cc206f9 Binary files /dev/null and b/doc/perspective.assets/vanish_point.png differ diff --git a/doc/perspective.assets/vanish_point_distance.png b/doc/perspective.assets/vanish_point_distance.png new file mode 100644 index 000000000..e468fcfa3 Binary files /dev/null and b/doc/perspective.assets/vanish_point_distance.png differ diff --git a/main.py b/main.py new file mode 100644 index 000000000..eb9dd39b7 --- /dev/null +++ b/main.py @@ -0,0 +1,83 @@ +from module.config.config import AzurLaneConfig +from module.logger import logger + + +class AzurLaneAutoScript: + def __init__(self, config_name='main'): + self.config = AzurLaneConfig(config_name) + + def setting(self): + for key, value in self.config.config['Setting'].items(): + print(f'{key} = {value}') + + logger.hr('Settings saved') + + def main(self): + """ + Method to run main chapter. + """ + from module.campaign.run import CampaignRun + az = CampaignRun(self.config) + az.run(self.config.CAMPAIGN_NAME) + + def daily(self): + """ + Method to run daily missions. + """ + if self.config.ENABLE_DAILY_MISSION: + from module.daily.daily import Daily + az = Daily(self.config) + if not az.record_executed_since(): + az.run() + az.record_save() + + if self.config.ENABLE_HARD_CAMPAIGN: + from module.hard.hard import CampaignHard + az = CampaignHard(self.config) + if not az.record_executed_since(): + az.run() + az.record_save() + + if self.config.ENABLE_EXERCISE: + from module.exercise.exercise import Exercise + az = Exercise(self.config) + if not az.record_executed_since(): + az.run() + az.record_save() + + def event(self): + """ + Method to run event. + """ + from module.campaign.run import CampaignRun + az = CampaignRun(self.config) + az.run(self.config.CAMPAIGN_EVENT) + + def event_daily_ab(self): + from module.event.campaign_ab import CampaignAB + az = CampaignAB(self.config) + az.run_event_daily() + + def semi_auto(self): + from module.daemon.daemon import AzurLaneDaemon + az = AzurLaneDaemon(self.config) + az.daemon() + + def c72_mystery_farming(self): + from module.campaign.run import CampaignRun + az = CampaignRun(self.config) + az.run('campaign_7_2_mystery_farming') + + def c124_leveling(self): + from module.campaign.run import CampaignRun + az = CampaignRun(self.config) + az.run('campaign_12_4_leveling') + + def retire(self): + from module.retire.retirement import Retirement + az = Retirement(self.config) + az.retire_ships(amount=2000) + + +# alas = AzurLaneAutoScript() +# alas.retire() diff --git a/main.pyw b/main.pyw new file mode 100644 index 000000000..d0ed6ca76 --- /dev/null +++ b/main.pyw @@ -0,0 +1,244 @@ +import codecs +import configparser +import os + +from gooey import Gooey, GooeyParser + +from main import AzurLaneAutoScript +from module.config.dictionary import dic_chi_to_eng, dic_eng_to_chi + +running = True + + +@Gooey( + optional_cols=2, + program_name="AzurLaneAutoScript", + sidebar_title='功能', + terminal_font_family='Consolas', + language='chinese', + default_size=(800, 850), + navigation='SIDEBAR', + tabbed_groups=True, + show_success_modal=False, + show_failure_modal=False, + # show_stop_warning=False, + # load_build_config='gooey_config.json', + # dump_build_config='gooey_config.json', +) +def main(): + script_name = os.path.splitext(os.path.basename(__file__))[0] + + # Load default value from .ini file. + config_file = f'./config/{script_name}.ini' + config = configparser.ConfigParser(interpolation=None) + config.read_file(codecs.open(config_file, "r", "utf8")) + + saved_config = {} + for opt, option in config.items(): + for key, value in option.items(): + key = dic_eng_to_chi.get(key, key) + if value in dic_eng_to_chi: + value = dic_eng_to_chi.get(value, value) + if value == 'None': + value = '' + + saved_config[key] = value + + def default(name): + """Get default value in .ini file. + Args: + name (str): option, in chinese. + + Returns: + str: Default value, in chinese. + """ + name = name.strip('-') + return saved_config.get(name, '') + + def choice_list(total): + return [str(index) for index in range(1, total + 1)] + + # Don't use checkbox in gooey, use drop box instead. + # https://github.com/chriskiehl/Gooey/issues/148 + # https://github.com/chriskiehl/Gooey/issues/485 + + parser = GooeyParser(description='An Azur Lane Automation Tool.') + subs = parser.add_subparsers(help='commands', dest='command') + + # ==========出击设置========== + setting_parser = subs.add_parser('出击设置') + + # 选择关卡 + stage = setting_parser.add_argument_group('关卡设置', '需要运行一次来保存选项') + stage.add_argument('--启用停止条件', default=default('--启用停止条件'), choices=['是', '否']) + + stop = stage.add_argument_group('停止条件', '触发后不会马上停止会先完成当前出击, 不需要就填0') + stop.add_argument('--如果出击次数大于', default=default('--如果出击次数大于'), help='会沿用先前设置, 完成出击将扣除次数, 直至清零') + stop.add_argument('--如果时间超过', default=default('--如果时间超过'), help='使用未来24小时内的时间, 使用会沿用先前设置, 直至触发. 建议提前10分钟左右, 以完成当前出击. 格式 14:59') + stop.add_argument('--如果石油低于', default=default('--如果石油低于')) + stop.add_argument('--如果触发心情控制', default=default('--如果触发心情控制'), choices=['是', '否'], help='触发后会等自动等待心情回复') + stop.add_argument('--如果船舱已满', default=default('--如果船舱已满'), choices=['是', '否']) + + # 出击舰队 + fleet = setting_parser.add_argument_group('出击舰队', '暂不支持阵型选择, 暂不支持备用道中队') + fleet.add_argument('--启用舰队控制', default=default('--启用舰队控制'), choices=['是', '否']) + + f1 = fleet.add_argument_group('道中队') + f1.add_argument('--舰队编号1', default=default('--舰队编号1'), choices=['1', '2', '3', '4', '5', '6']) + f1.add_argument('--舰队阵型1', default=default('--舰队阵型1'), choices=['单纵阵', '复纵阵', '轮形阵']) + + f2 = fleet.add_argument_group('BOSS队') + f2.add_argument('--舰队编号2', default=default('--舰队编号2'), choices=['不使用', '1', '2', '3', '4', '5', '6']) + f2.add_argument('--舰队阵型2', default=default('--舰队阵型2'), choices=['单纵阵', '复纵阵', '轮形阵']) + + f3 = fleet.add_argument_group('备用道中队') + f3.add_argument('--舰队编号3', default=default('--舰队编号3'), choices=['不使用', '1', '2', '3', '4', '5', '6']) + f3.add_argument('--舰队阵型3', default=default('--舰队阵型3'), choices=['单纵阵', '复纵阵', '轮形阵']) + + # 潜艇设置 + submarine = setting_parser.add_argument_group('潜艇设置', '暂不支持潜艇, 会避免潜艇进图') + submarine.add_argument('--舰队编号4', default=default('--舰队编号4'), choices=['不使用', '1', '2']) + submarine.add_argument('--潜艇出击方案', default=default('--潜艇出击方案'), choices=['不使用', '仅狩猎', '每战出击', '空弹出击', 'BOSS战出击', 'BOSS战BOSS出现后召唤']) + + # 心情控制 + emotion = setting_parser.add_argument_group('心情控制') + emotion.add_argument('--启用心情消耗', default=default('--启用心情消耗'), choices=['是', '否']) + emotion.add_argument('--无视红脸出击警告', default=default('--无视红脸出击警告'), choices=['是', '否']) + + e1 = emotion.add_argument_group('道中队') + e1.add_argument('--心情回复1', default=default('--心情回复1'), choices=['未放置于后宅', '后宅一楼', '后宅二楼']) + e1.add_argument('--心情控制1', default=default('--心情控制1'), choices=['保持经验加成', '防止绿脸', '防止黄脸', '防止红脸']) + e1.add_argument('--全员已婚1', default=default('--全员已婚1'), choices=['是', '否']) + + e2 = emotion.add_argument_group('BOSS队') + e2.add_argument('--心情回复2', default=default('--心情回复2'), choices=['未放置于后宅', '后宅一楼', '后宅二楼']) + e2.add_argument('--心情控制2', default=default('--心情控制2'), choices=['保持经验加成', '防止绿脸', '防止黄脸', '防止红脸']) + e2.add_argument('--全员已婚2', default=default('--全员已婚2'), choices=['是', '否']) + + e3 = emotion.add_argument_group('备用道中队', '会在主队触发心情控制时使用') + e3.add_argument('--心情回复3', default=default('--心情回复3'), choices=['未放置于后宅', '后宅一楼', '后宅二楼']) + e3.add_argument('--心情控制3', default=default('--心情控制3'), choices=['保持经验加成', '防止绿脸', '防止黄脸', '防止红脸']) + e3.add_argument('--全员已婚3', default=default('--全员已婚3'), choices=['是', '否']) + + # 血量平衡 + balance = setting_parser.add_argument_group('血量平衡', '目前无效, 不要使用') + balance.add_argument('--启用血量平衡', default=default('--启用血量平衡'), choices=['是', '否']) + + # 退役选项 + retire = setting_parser.add_argument_group('退役设置', '') + retire.add_argument('--启用退役', default=default('--启用退役'), choices=['是', '否']) + retire.add_argument('--退役方案', default=default('--退役方案'), choices=['退役全部', '退役10个']) + + rarity = retire.add_argument_group('退役稀有度', '暂不支持舰种选择') + rarity.add_argument('--退役白皮', default=default('--退役白皮'), choices=['是', '否'], help='N') + rarity.add_argument('--退役蓝皮', default=default('--退役蓝皮'), choices=['是', '否'], help='R') + rarity.add_argument('--退役紫皮', default=default('--退役紫皮'), choices=['是', '否'], help='SR') + rarity.add_argument('--退役金皮', default=default('--退役金皮'), choices=['是', '否'], help='SSR') + + # 掉落记录 + drop = setting_parser.add_argument_group('掉落记录', '保存掉落物品的截图, 启用后会减慢结算时的点击速度') + drop.add_argument('--启用掉落记录', default=default('--启用掉落记录'), choices=['是', '否']) + drop.add_argument('--掉落保存目录', default=default('--掉落保存目录')) + + # 模拟器 + emulator = setting_parser.add_argument_group('模拟器', '') + emulator.add_argument('--设备', default=default('--设备'), help='例如 127.0.0.1:62001') + + # ==========每日任务========== + daily_parser = subs.add_parser('每日任务') + + # 选择每日 + daily = daily_parser.add_argument_group('选择每日', '每日任务, 演习, 困难图') + daily.add_argument('--打每日', default=default('--打每日'), help='若当天有记录, 则跳过', choices=['是', '否']) + daily.add_argument('--打困难', default=default('--打困难'), help='若当天有记录, 则跳过', choices=['是', '否']) + daily.add_argument('--打演习', default=default('--打演习'), help='若在刷新后有记录, 则跳过', choices=['是', '否']) + + # 每日设置 + daily_task = daily_parser.add_argument_group('每日设置', '不支持潜艇每日') + daily_task.add_argument('--战术研修', default=default('--战术研修'), choices=['航空', '炮击', '雷击']) + daily_task.add_argument('--斩首行动', default=default('--斩首行动'), choices=['70级', '50级', '35级']) + daily_task.add_argument('--商船护航', default=default('--商船护航'), choices=['70级', '50级', '35级']) + daily_task.add_argument('--海域突进', default=default('--海域突进'), choices=['70级', '50级', '35级']) + daily_task.add_argument('--每日舰队', default=default('--每日舰队'), choices=['1', '2', '3', '4', '5', '6']) + daily_task.add_argument('--每日舰队快速换装', default=default('--每日舰队快速换装'), help='打之前换装备, 打完后卸装备, 不需要就填0\n逗号分割, 例如 \"3, 1, 0, 1, 1, 0\"') + + # 困难设置 + hard = daily_parser.add_argument_group('困难设置', '暂时仅支持 10-4') + hard.add_argument('--困难地图', default=default('--困难地图'), help='比如 10-4') + hard.add_argument('--困难舰队', default=default('--困难舰队'), choices=['1', '2']) + hard.add_argument('--困难舰队快速换装', default=default('--困难舰队快速换装'), help='打之前换装备, 打完后卸装备, 不需要就填0\n逗号分割, 例如 \"3, 1, 0, 1, 1, 0\"') + + # 演习设置 + exercise = daily_parser.add_argument_group('演习设置', '暂时不支持挑选福利队') + exercise.add_argument('--演习对手选择', default=default('--演习对手选择'), choices=['经验最多', '排名最前', '福利队'], help='暂时仅支持经验最多') + exercise.add_argument('--演习次数保留', default=default('--演习次数保留'), help='暂时仅支持保留0个') + exercise.add_argument('--演习尝试次数', default=default('--演习尝试次数'), help='每个对手的尝试次数, 打不过就换') + exercise.add_argument('--演习SL阈值', default=default('--演习SL阈值'), help='HP<阈值时撤退') + exercise.add_argument('--演习低血量确认时长', default=default('--演习低血量确认时长'), help='HP低于阈值后, 过一定时长才会撤退\n推荐 1.0 ~ 3.0') + exercise.add_argument('--演习快速换装', default=default('--演习快速换装'), help='打之前换装备, 打完后卸装备, 不需要就填0\n逗号分割, 例如 \"3, 1, 0, 1, 1, 0\"') + + # ==========主线图========== + main_parser = subs.add_parser('主线图') + # 选择关卡 + stage = main_parser.add_argument_group('选择关卡', '主线图出击, 目前仅支持7-2') + stage.add_argument('--主线地图出击', default=default('--主线地图出击'), help='例如 7-2') + + # ==========活动图========== + event_parser = subs.add_parser('活动图') + + event = event_parser.add_argument_group('选择关卡', '') + event.add_argument('--活动关卡出击', default=default('--活动关卡出击'), help='例如 event_20200312cn_sp3') + + # ==========活动AB图每日三倍========== + event_ab_parser = subs.add_parser('活动AB图每日三倍') + event_name = event_ab_parser.add_argument_group('选择活动', '') + event_folder = [dic_eng_to_chi.get(f, f) for f in os.listdir('./campaign') if f.startswith('event_')][::-1] + event_name.add_argument('--活动名称ab', default=default('--活动名称ab'), choices=event_folder, help='例如 event_20200326_cn') + + # ==========半自动========== + semi_parser = subs.add_parser('半自动') + semi = semi_parser.add_argument_group('半自动模式', '手动选敌, 自动结算, 用于出击未适配的图') + semi.add_argument('--进图准备', default=default('--进图准备'), help='', choices=['是', '否']) + semi.add_argument('--跳过剧情', default=default('--跳过剧情'), help='注意, 这会自动确认所有提示框, 包括红脸出击', choices=['是', '否']) + + # ==========7-2三战拣垃圾========== + c_7_2_parser = subs.add_parser('7-2三战拣垃圾') + # c_12_4 = c_12_4_parser.add_argument_group('7-2三战拣垃圾', '需保证队伍有一定强度') + # c_12_4.add_argument('--非大型敌人进图忍耐', default=default('--非大型敌人进图忍耐'), choices=['0', '1', '2'], help='忍受进场多少战没有大型') + # c_12_4.add_argument('--非大型敌人撤退忍耐', default=default('--非大型敌人撤退忍耐'), choices=['0', '1', '2', '10'], + # help='没有大型之后还会打多少战, 不挑敌人选10') + # c_12_4.add_argument('--拣弹药124', default=default('--拣弹药124'), choices=['2', '3', '4', '5'], help='多少战后拣弹药') + + # ==========12-4打大型练级========== + c_12_4_parser = subs.add_parser('12-4打大型练级') + c_12_4 = c_12_4_parser.add_argument_group('12-4索敌设置', '需保证队伍有一定强度') + c_12_4.add_argument('--非大型敌人进图忍耐', default=default('--非大型敌人进图忍耐'), choices=['0', '1', '2'], help='忍受进场多少战没有大型') + c_12_4.add_argument('--非大型敌人撤退忍耐', default=default('--非大型敌人撤退忍耐'), choices=['0', '1', '2', '10'], help='没有大型之后还会打多少战, 不挑敌人选10') + c_12_4.add_argument('--拣弹药124', default=default('--拣弹药124'), choices=['2', '3', '4', '5'], help='多少战后拣弹药') + + + + args = parser.parse_args() + + # Convert option from chinese to english. + out = {} + for key, value in vars(args).items(): + key = dic_chi_to_eng.get(key, key) + value = dic_chi_to_eng.get(value, value) + out[key] = value + args = out + + # Update option to .ini file. + command = args['command'].capitalize() + config['Command']['command'] = command + for key, value in args.items(): + config[command][key] = str(value) + config.write(codecs.open(config_file, "w+", "utf8")) + + # Call AzurLaneAutoScript + alas = AzurLaneAutoScript(config_name=script_name) + alas.__getattribute__(command.lower())() + + +if __name__ == '__main__': + main() diff --git a/module/base/base.py b/module/base/base.py new file mode 100644 index 000000000..58dd03c56 --- /dev/null +++ b/module/base/base.py @@ -0,0 +1,87 @@ +from module.base.button import Button +from module.base.timer import Timer +from module.config.config import AzurLaneConfig +from module.device.device import Device + + +class ModuleBase: + def __init__(self, config, device=None): + """ + Args: + config (AzurLaneConfig): + device (Device): + """ + self.config = config + self.interval_timer = {} + if device is not None: + self.device = device + else: + self.device = Device(config=config) + + def appear(self, button, offset=0, interval=0): + """ + Args: + button (Button, Template): + offset (bool, int): + interval (int, float): interval between two active events. + + Returns: + bool: + """ + if interval: + if button.name not in self.interval_timer: + self.interval_timer[button.name] = Timer(interval) + if not self.interval_timer[button.name].reached(): + return False + + if offset: + if isinstance(offset, bool): + offset = self.config.BUTTON_OFFSET + appear = button.match(self.device.image, offset=offset, threshold=self.config.BUTTON_MATCH_SIMILARITY) + else: + appear = button.appear_on(self.device.image, threshold=self.config.COLOR_SIMILAR_THRESHOLD) + + if appear and interval: + self.interval_timer[button.name].reset() + + return appear + + def appear_then_click(self, button, screenshot=False, genre='items', offset=0, interval=0): + appear = self.appear(button, offset=offset, interval=interval) + if appear: + if screenshot: + self.device.sleep(self.config.WAIT_BEFORE_SAVING_SCREEN_SHOT) + self.device.screenshot() + self.device.save_screenshot(genre=genre) + self.device.click(button) + return appear + + def wait_until_appear(self, button, offset=0): + while 1: + self.device.screenshot() + if self.appear(button, offset=offset): + break + + def wait_until_appear_then_click(self, button, offset=0): + self.wait_until_appear(button, offset=offset) + self.device.click(button) + + def wait_until_disappear(self, button, offset=0): + while 1: + self.device.screenshot() + if not self.appear(button, offset=offset): + break + + def image_area(self, button): + """Extract the area from image. + + Args: + button(Button): Button instance. + + Returns: + PIL.Image.Image: + """ + return self.device.image.crop(button.area) + + def interval_reset(self, button): + self.interval_timer[button.name].reset() diff --git a/module/base/button.py b/module/base/button.py new file mode 100644 index 000000000..0e4fe9515 --- /dev/null +++ b/module/base/button.py @@ -0,0 +1,150 @@ +import os +import traceback + +import cv2 +from PIL import Image + +from module.base.utils import * + + +class Button: + def __init__(self, area, color, button, file=None, name=None): + """Initialize a Button instance. + + Args: + area (tuple): Area that the button would appear on the image. + (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y) + color (tuple): Color we expect the area would be. + (r, g, b) + button (tuple): Area to be click if button appears on the image. + (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y) + If tuple is empty, this object can be use as a checker. + Examples: + BATTLE_PREPARATION = Button( + area=(1562, 908, 1864, 1003), + color=(231, 181, 90), + button=(1562, 908, 1864, 1003) + ) + """ + self.area = area + self.color = color + self._button = button + self._button_offset = None + self._match_init = False + self.file = file + self.image = None + if file: + self.name = os.path.splitext(os.path.split(file)[1])[0] + elif name: + self.name = name + else: + (filename, line_number, function_name, text) = traceback.extract_stack()[-2] + self.name = text[:text.find('=')].strip() + + def __str__(self): + return self.name + + __repr__ = __str__ + + def __eq__(self, other): + return str(self) == str(other) + + @property + def button(self): + if self._button_offset is None: + return self._button + else: + return self._button_offset + + def appear_on(self, image, threshold=10): + """Check if the button appears on the image. + + Args: + image (PIL.Image.Image): Screenshot. + threshold (int): Default to 10. + + Returns: + bool: True if button appears on screenshot. + """ + return color_similar( + color1=get_color(image, self.area), + color2=self.color, + threshold=threshold + ) + + def load_color(self, image): + """Load color from the specific area of the given image. + This method is irreversible, this would be only use in some special occasion. + + Args: + image (PIL.Image.Image): Another screenshot. + + Returns: + tuple: Color (r, g, b). + """ + color = get_color(image, self.area) + self.color = color + return color + + def ensure_template(self): + """ + Load asset image. + If needs to call self.match, call this first. + """ + if not self._match_init: + self.image = np.array(Image.open(self.file).crop(self.area).convert('RGB')) + self._match_init = True + + def match(self, image, offset=30, threshold=0.85): + """Detects button by template matching. To Some button, its location may not be static. + + Args: + image: Screenshot. + offset (int, tuple): Detection area offset. + threshold (float): 0-1. Similarity. + + Returns: + bool. + """ + self.ensure_template() + + if isinstance(offset, tuple): + offset = np.array((-offset[0], -offset[1], offset[0], offset[1])) + else: + offset = np.array((0, -offset, 0, offset)) + + # offset = np.array((0, -offset, 0, offset)) + # offset = np.array((-offset, -offset, offset, offset)) + image = np.array(image.crop(offset + self.area)) + res = cv2.matchTemplate(self.image, image, cv2.TM_CCOEFF_NORMED) + _, similarity, _, point = cv2.minMaxLoc(res) + + self._button_offset = area_offset(self._button, offset[:2] + np.array(point)) + + return similarity > threshold + + +class ButtonGrid: + def __init__(self, origin, delta, button_shape, grid_shape, name=None): + self.origin = np.array(origin) + self.delta = np.array(delta) + self.button_shape = np.array(button_shape) + self.grid_shape = np.array(grid_shape) + if name: + self._name = name + else: + (filename, line_number, function_name, text) = traceback.extract_stack()[-2] + self._name = text[:text.find('=')].strip() + + def __getitem__(self, item): + base = np.round(np.array(item) * self.delta + self.origin).astype(int) + area = tuple(np.append(base, base + self.button_shape)) + return Button(area=area, color=(), button=area, name='%s_%s_%s' % (self._name, item[0], item[1])) + + def generate(self): + for y in range(self.grid_shape[1]): + for x in range(self.grid_shape[0]): + yield x, y, self[x, y] + + def buttons(self): + return list([button for _, _, button in self.generate()]) diff --git a/module/base/ocr.py b/module/base/ocr.py new file mode 100644 index 000000000..1687d92c2 --- /dev/null +++ b/module/base/ocr.py @@ -0,0 +1,160 @@ +import time + +import cv2 +import numpy as np +from PIL import Image +from cnocr import CnOcr + +from module.base.button import Button +from module.base.utils import extract_letters +from module.logger import logger + +OCR_MODELS = { + # Font: Impact, AgencyFB + # Charset: 0123456789 + 'digit': CnOcr(root='./cnocr_models/digit', model_epoch=60), + # Font: Impact + # Charset: 0123456789ABCDEFSP-:/ + 'stage': CnOcr(root='./cnocr_models/stage', model_epoch=56), +} +image_shape = (280, 32) +width_range = (0.6, 1.4) +text_length = (1, 6) +text_interval = (0, 10) +y_range = (-2, 2) + + +class Ocr: + def __init__(self, buttons, lang, letter=(255, 255, 255), back=(0, 0, 0), mid_process_height=70, threshold=127, + additional_preprocess=None, length=None, white_list=None, name='OCR'): + """ + Args: + lang (str): OCR model. in ['digit', 'cnocr']. + letter (tuple(int)): Letter RGB. + back (tuple(int)): Background RGB. + mid_process_height (int): 70 + additional_preprocess (callable): + length (int, tuple(int)): Expected length. + white_list (str): Expected str. + buttons (Button, List[Button]): Button or list of Button instance. + """ + self.lang = lang + self.cnocr = OCR_MODELS[lang] + self.letter = letter + self.back = back + self.mid_process_height = mid_process_height + self.threshold = threshold + self.additional_preprocess = additional_preprocess + self.length = (length, length) if isinstance(length, int) else length + self.white_list = white_list + self.buttons = buttons if isinstance(buttons, list) else [buttons] + self.name = str(buttons) if isinstance(buttons, Button) else name + + def additional_preprocess_example(self, image): + """ + Args: + image (np.ndarray): data range: [0, 255], dtype: float. shape: [?, 70] + + Returns: + np.ndarray: data range: [0, 255], dtype: float. + """ + pass + + def pre_process(self, image): + """ + Args: + image: A cropped screenshot. + + Returns: + np.ndarray: shape: [70, 280]. data range: [0, 1] + """ + # Resize to height=70. + size = (int(image.size[0] / image.size[1] * self.mid_process_height), self.mid_process_height) + image = image.resize(size, Image.BILINEAR) + + # Set letter color to black, set background color to white. + image = extract_letters(image, letter=self.letter, back=self.back) + + # Additional preprocess. + if self.additional_preprocess is not None: + image = self.additional_preprocess(image) + + # Binarization. + _, image = cv2.threshold(image, self.threshold, 255, cv2.THRESH_BINARY) + + # Resize to input size. + size = (int(image.shape[1] / image.shape[0] * image_shape[1]), image_shape[1]) + image = cv2.resize(image, size, interpolation=cv2.INTER_LINEAR) + diff_x = image_shape[0] - image.shape[1] + if diff_x > 0: + image = np.pad(image, ((0, 0), (0, diff_x)), mode='constant', constant_values=255) + else: + image = image[:, :image_shape[0]] + + # Image.fromarray(image.astype('uint8')).show() + + return image / 255.0 + + def after_process(self, result): + """ + Args: + result (list[str]): ['第', '二', '行'] + + Returns: + str: + """ + result = ''.join(result) + + if self.length is not None: + if len(result) > self.length[1] or len(result) < self.length[0]: + logger.warning(f'OCR result length unexpected. Expect: {self.length}. Result: {len(result)}') + if self.white_list: + for letter in result: + if letter not in self.white_list: + logger.warning(f'OCR letter unexpected. Letter: {letter}. White_list: {self.white_list}') + + return result + + def ocr(self, image): + start_time = time.time() + + image_list = [self.pre_process(image.crop(button.area)) for button in self.buttons] + result_list = self.cnocr.ocr_for_single_lines(image_list) + result_list = [self.after_process(result) for result in result_list] + + if len(self.buttons) == 1: + result_list = result_list[0] + logger.attr(name='%s %ss' % (self.name, str(round(time.time() - start_time, 3)).ljust(5, '0')), + text=str(result_list)) + + return result_list + + +class Digit(Ocr): + def __init__(self, buttons, letter=(255, 255, 255), back=(0, 0, 0), mid_process_height=70, threshold=127, + additional_preprocess=None, length=None, white_list=None, limit=None, name='OCR'): + super().__init__(buttons=buttons, lang='digit', letter=letter, back=back, mid_process_height=mid_process_height, + threshold=threshold, + additional_preprocess=additional_preprocess, length=length, white_list=white_list, name=name) + self.limit = (0, limit) if isinstance(limit, int) else limit + + def after_process(self, raw): + """ + Returns: + int: + """ + raw = super().after_process(raw) + if not raw: + result = 0 + else: + result = int(raw) + + if self.limit: + if result < self.limit[0]: + logger.info(f'OCR result smaller than expected. Expect: {self.limit}. Raw: {raw}. Treat as: {result}') + result = self.limit[0] + if result > self.limit[1]: + logger.info(f'OCR result bigger than expected. Expect: {self.limit}. Raw: {raw}. Treat as: {result}') + result = self.limit[1] + + return result diff --git a/module/base/template.py b/module/base/template.py new file mode 100644 index 000000000..050f87f35 --- /dev/null +++ b/module/base/template.py @@ -0,0 +1,65 @@ +import cv2 +import numpy as np +from PIL import Image + + +class Template: + def __init__(self, area, color, button, file): + """ + Args: + file(str): Relative path to file. + """ + + # self.area = image.getbbox() + self.area = area + self.color = color + self.button = button + image = Image.open(file) + self.image = np.array(image.crop(self.area)) + self.similarity = 0.85 + self.preprocess_func = None + + def set_preprocess_func(self, func): + """ + Args: + func: Image preprocess function + """ + self.preprocess_func = func + self.image = self._preprocess(self.image) + + def _preprocess(self, image): + image = image.astype(float) + image = self.preprocess_func(image) + image[image > 255] = 255 + image[image < 0] = 0 + image = image.astype('uint8') + return image + + def match(self, image): + """ + Args: + image: Full-screen screenshot. + + Returns: + bool: True if template matched. + """ + image = np.array(image.crop(self.area)) + if self.preprocess_func is not None: + image = self._preprocess(image) + + res = cv2.matchTemplate(np.array(image), self.image, cv2.TM_CCOEFF_NORMED) + _, similarity, _, _ = cv2.minMaxLoc(res) + + return similarity > self.similarity + + +def preprocess_func_example(image): + """ + Args: + image (np.ndarray): + + Returns: + np.ndarray + """ + image = (image - 64) / 0.75 + return image diff --git a/module/base/timer.py b/module/base/timer.py new file mode 100644 index 000000000..454cf4634 --- /dev/null +++ b/module/base/timer.py @@ -0,0 +1,49 @@ +import time +from functools import wraps + +from module.logger import logger + + +def timer(function): + @wraps(function) + def function_timer(*args, **kwargs): + t0 = time.time() + + result = function(*args, **kwargs) + t1 = time.time() + print('%s: %s s' % (function.__name__, str(round(t1 - t0, 10)))) + return result + return function_timer + + +class Timer: + def __init__(self, limit): + self.limit = limit + self._current = 0 + + def start(self): + if not self.started(): + self._current = time.time() + + def started(self): + return bool(self._current) + + def current(self): + """ + Returns: + float + """ + return time.time() - self._current + + def reached(self): + """ + Returns: + bool + """ + return time.time() - self._current > self.limit + + def reset(self): + self._current = time.time() + + def show(self): + logger.info('%s s' % str(self.current())) diff --git a/module/base/utils.py b/module/base/utils.py new file mode 100644 index 000000000..3b51b50e9 --- /dev/null +++ b/module/base/utils.py @@ -0,0 +1,301 @@ +import numpy as np +from PIL import ImageStat + + +def random_normal_distribution_int(a, b, n=5): + """Generate a normal distribution int within the interval. Use the average value of several random numbers to + simulate normal distribution. + + Args: + a (int): The minimum of the interval. + b (int): The maximum of the interval. + n (int): The amount of numbers in simulation. Default to 5. + + Returns: + int + """ + if a < b: + output = np.mean(np.random.randint(a, b, size=n)) + return int(output.round()) + else: + return b + + +def ensure_time(second, n=5, precision=3): + """Ensure to be time. + + Args: + second (int, float, tuple): time. + n (int): The amount of numbers in simulation. Default to 5. + precision (int): Decimals. + + Returns: + + """ + if isinstance(second, tuple): + multiply = 10 ** precision + return random_normal_distribution_int(second[0] * multiply, second[1] * multiply, n) / multiply + else: + return second + + +def random_rectangle_point(area): + """Choose a random point in an area. + + Args: + area (tuple): (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + + Returns: + int: x + int: y + """ + x = random_normal_distribution_int(area[0], area[2]) + y = random_normal_distribution_int(area[1], area[3]) + return x, y + + +def area_offset(area, offset): + """ + + Args: + area(tuple): (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + offset(tuple): (x, y). + + Returns: + tuple: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + """ + return tuple(np.array(area) + np.append(offset, offset)) + + +def area_pad(area, pad=10): + """ + + Args: + area(tuple): (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + pad(int): + + Returns: + tuple: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + """ + return tuple(np.array(area) + np.array([pad, pad, -pad, -pad])) + + +def point_in_area(point, area, threshold=5): + """ + + Args: + point: (x, y). + area: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + threshold: int + + Returns: + bool + """ + return area[0] - threshold < point[0] < area[2] + threshold and area[1] - threshold < point[1] < area[3] + threshold + + +def area_in_area(area1, area2, threshold=5): + """ + + Args: + area1: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + area2: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + threshold: int + + Returns: + bool + """ + return area2[0] - threshold <= area1[0] \ + and area2[1] - threshold <= area1[1] \ + and area1[2] <= area2[2] + threshold \ + and area1[3] <= area2[3] + threshold + + +def area_cross_area(area1, area2, threshold=5): + """ + + Args: + area1: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + area2: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + threshold: int + + Returns: + bool + """ + return point_in_area((area1[0], area1[1]), area2, threshold=threshold) \ + or point_in_area((area1[2], area1[1]), area2, threshold=threshold) \ + or point_in_area((area1[0], area1[3]), area2, threshold=threshold) \ + or point_in_area((area1[2], area1[3]), area2, threshold=threshold) + + +def node2location(node): + """ + Args: + node(str): Example: 'E3' + + Returns: + tuple: Example: (6, 4) + """ + return ord(node[0]) % 32 - 1, int(node[1]) - 1 + + +def location2node(location): + """ + Args: + location(tuple): Example: (6, 4) + + Returns: + str: Example: 'E3' + """ + return chr(location[0] + 64 + 1) + str(location[1] + 1) + + +def get_color(image, area): + """Calculate the average color of a particular area of the image. + + Args: + image (PIL.Image.Image): Screenshot. + area (tuple): (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y) + + Returns: + tuple: (r, g, b) + """ + temp = image.crop(area) + stat = ImageStat.Stat(temp) + return np.array(stat.mean) + + +def color_similarity(color1, color2): + """ + Args: + color1 (tuple): (r, g, b) + color2 (tuple): (r, g, b) + + Returns: + int: + """ + diff = np.array(color1) - np.array(color2) + positive, negative = diff, np.abs(diff) + positive[diff < 0] = 0 + negative[diff > 0] = 0 + diff = np.max(positive) + np.max(negative) + return diff + + +def color_similar(color1, color2, threshold=10): + """Consider two colors are similar, if tolerance lesser or equal threshold. + Tolerance = Max(Positive(difference_rgb)) + Max(- Negative(difference_rgb)) + The same as the tolerance in Photoshop. + + Args: + color1 (tuple): (r, g, b) + color2 (tuple): (r, g, b) + threshold (int): Default to 10. + + Returns: + bool: True if two colors are similar. + """ + # print(color1, color2) + diff = np.array(color1) - np.array(color2) + positive, negative = diff, np.abs(diff) + positive[diff < 0] = 0 + negative[diff > 0] = 0 + diff = np.max(positive) + np.max(negative) + return diff <= threshold + + +def color_similar_1d(bar, color, threshold=10): + """ + Args: + bar: 1D array. + color: (r, g, b) + threshold(int): Default to 10. + + Returns: + np.ndarray: bool + """ + diff = np.array(bar) - np.array(color) + positive, negative = diff, np.abs(diff) + positive[diff < 0] = 0 + negative[diff > 0] = 0 + diff = np.max(positive, axis=1) + np.max(negative, axis=1) + return diff <= threshold + + +def color_similarity_2d(image, color): + """ + Args: + image: 2D array. + color: (r, g, b) + + Returns: + np.ndarray: uint8 + """ + diff = np.array(image) - color + positive, negative = diff, np.abs(diff) + positive[diff < 0] = 0 + negative[diff > 0] = 0 + diff = 255.0 - np.max(positive, axis=2) - np.max(negative, axis=2) + diff[diff < 0] = 0 + image = diff.astype(np.uint8) + return image + + +def extract_letters(image, letter=(255, 255, 255), back=(0, 0, 0)): + """Set letter color to black, set background color to white. + + Args: + image: Shape (height, width, channel) + letter (tuple): Letter RGB. + back (tuple): Background RGB. + + Returns: + np.ndarray: Shape (height, width) + """ + image = color_similarity_2d(np.array(image), color=letter) + back = color_similarity(back, letter) + image = (255.0 - image) * (1 + back / 255) + image[image > 255] = 255 + return image + + +def red_overlay_transparency(color1, color2, red=247): + """Calculate the transparency of red overlay. + + Args: + color1: origin color. + color2: changed color. + red(int): red color 0-255. Default to 247. + + Returns: + float: 0-1 + """ + return (color2[0] - color1[0]) / (red - color1[0]) + + +def color_bar_percentage(image, area, prev_color, reverse=False, starter=0, threshold=30): + """ + Args: + image: + area: + reverse: True if bar goes from right to left. + starter: + prev_color: + + Returns: + float: 0 to 1. + """ + bar = np.array(image.crop(area)) + length = bar.shape[1] + bar = np.swapaxes(bar, 0, 1) + bar = bar[::-1, :, :] if reverse else bar + prev_index = 0 + for index, color in enumerate(bar): + if index < starter: + continue + mask = color_similar_1d(color, prev_color, threshold=threshold) + if np.any(mask): + prev_color = color[mask].mean(axis=0) + prev_index = index + + return prev_index / length diff --git a/module/campaign/assets.py b/module/campaign/assets.py new file mode 100644 index 000000000..ee4a2c52f --- /dev/null +++ b/module/campaign/assets.py @@ -0,0 +1,14 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +C2 = Button(area=(471, 577, 512, 614), color=(117, 104, 72), button=(471, 577, 512, 614), file='./assets/campaign/C2.png') +CHAPTER_NEXT = Button(area=(1216, 362, 1232, 388), color=(121, 150, 198), button=(1216, 362, 1232, 388), file='./assets/campaign/CHAPTER_NEXT.png') +CHAPTER_PREV = Button(area=(42, 360, 58, 387), color=(105, 133, 169), button=(42, 360, 58, 387), file='./assets/campaign/CHAPTER_PREV.png') +D3 = Button(area=(842, 284, 881, 323), color=(104, 91, 63), button=(842, 284, 881, 323), file='./assets/campaign/D3.png') +EVENT_20200312CN_SP3 = Button(area=(797, 414, 839, 450), color=(118, 105, 72), button=(797, 414, 839, 450), file='./assets/campaign/EVENT_20200312CN_SP3.png') +MODE_CHANGE = Button(area=(69, 644, 131, 653), color=(216, 114, 114), button=(28, 647, 143, 699), file='./assets/campaign/MODE_CHANGE.png') +OCR_OIL = Button(area=(635, 28, 715, 48), color=(74, 75, 84), button=(635, 28, 715, 48), file='./assets/campaign/OCR_OIL.png') +TEMPLATE_STAGE_CLEAR = Template(area=(432, 201, 471, 240), color=(117, 104, 71), button=(432, 201, 471, 240), file='./assets/campaign/TEMPLATE_STAGE_CLEAR.png') diff --git a/module/campaign/campaign_base.py b/module/campaign/campaign_base.py new file mode 100644 index 000000000..48a75076b --- /dev/null +++ b/module/campaign/campaign_base.py @@ -0,0 +1,50 @@ +from module.base.button import Button +from module.logger import logger +from module.map.exception import CampaignEnd +from module.map.map import Map +from module.map.map_base import CampaignMap + + +class CampaignBase(Map): + FUNCTION_NAME_BASE = 'battle_' + ENTRANCE = Button(area=(), color=(), button=(), name='default_button') + MAP: CampaignMap + + def battle_default(self): + if self.clear_enemy(): + return True + + logger.warning('No battle executed.') + return False + + def execute_a_battle(self): + func = self.FUNCTION_NAME_BASE + 'default' + for extra_battle in range(10): + if hasattr(self, self.FUNCTION_NAME_BASE + str(self.battle_count - extra_battle)): + func = self.FUNCTION_NAME_BASE + str(self.battle_count - extra_battle) + break + + logger.hr(f'{self.FUNCTION_NAME_BASE}{self.battle_count}', level=2) + func = self.__getattribute__(func) + + result = func() + if not result: + logger.warning('No combat executed.') + exit(1) + + return result + + def run(self): + logger.hr(self.ENTRANCE, level=2) + self.handle_spare_fleet() + self.enter_map(self.ENTRANCE, mode=self.config.CAMPAIGN_MODE) + self.handle_map_fleet_lock() + self.handle_fleet_reverse() + self.map_init(self.MAP) + + for _ in range(15): + try: + self.execute_a_battle() + except CampaignEnd: + logger.hr('Campaign end') + break diff --git a/module/campaign/campaign_hard.py b/module/campaign/campaign_hard.py new file mode 100644 index 000000000..3b7f5cb71 --- /dev/null +++ b/module/campaign/campaign_hard.py @@ -0,0 +1,92 @@ +from module.base.timer import Timer +from module.campaign.campaign_base import CampaignBase +from module.hard.equipment import HardEquipment +from module.logger import logger +from module.map.assets import MAP_PREPARATION, FLEET_PREPARATION +from module.map.exception import CampaignEnd +from module.ui.ui import CAMPAIGN_CHECK + + +class Config: + MAP_HAS_AMBUSH = False + EMOTION_REDUCE = False + +class Campaign(CampaignBase, HardEquipment): + def run(self): + logger.hr(self.ENTRANCE, level=2) + self.enter_map(self.ENTRANCE, mode='hard') + self.map = self.MAP + self.map.reset() + + if self.config.FLEET_HARD == 1: + self.ensure_edge_insight(reverse=True) + self.full_scan_find_boss() + else: + self.fleet_switch_click() + self.ensure_no_info_bar() + self.ensure_edge_insight() + self.full_scan_find_boss() + + try: + self.clear_boss() + except CampaignEnd: + logger.hr('Campaign end') + + def fleet_preparation(self): + self.equipment_take_on() + + @property + def _expected_combat_end(self): + return 'in_stage' + + def clear_boss(self): + grids = self.map.select(is_boss=True) + grids = grids.add(self.map.select(may_boss=True, is_enemy=True)) + logger.info('May boss: %s' % self.map.select(may_boss=True)) + logger.info('May boss and is enemy: %s' % self.map.select(may_boss=True, is_enemy=True)) + logger.info('Is boss: %s' % self.map.select(is_boss=True)) + # logger.info('Grids: %s' % grids) + if grids: + logger.hr('Clear BOSS') + grids = grids.sort(cost=True, weight=True) + logger.info('Grids: %s' % str(grids)) + self._goto(grids[0]) + raise CampaignEnd('BOSS Clear.') + + return False + + def equipment_take_off_when_finished(self): + logger.info('equipment_take_off_when_finished') + campaign_timer = Timer(2) + map_timer = Timer(1) + fleet_timer = Timer(1) + + while 1: + self.device.screenshot() + + # Enter campaign + if campaign_timer.reached() and self.appear_then_click(self.ENTRANCE): + campaign_timer.reset() + continue + + # Map preparation + if map_timer.reached() and self.appear(MAP_PREPARATION): + self.device.click(MAP_PREPARATION) + map_timer.reset() + campaign_timer.reset() + continue + + # Fleet preparation + if fleet_timer.reached() and self.appear(FLEET_PREPARATION): + self.equipment_take_off() + self.ui_back(check_button=CAMPAIGN_CHECK, appear_button=FLEET_PREPARATION) + break + + # Retire + if self.handle_retirement(): + continue + + # Emotion + pass + + return True diff --git a/module/campaign/campaign_ocr.py b/module/campaign/campaign_ocr.py new file mode 100644 index 000000000..6eee12581 --- /dev/null +++ b/module/campaign/campaign_ocr.py @@ -0,0 +1,162 @@ +import collections + +import cv2 +import numpy as np + +from module.base.ocr import Ocr +from module.base.utils import extract_letters, area_offset +from module.campaign.assets import * +from module.logger import logger + + +def ensure_chapter_index(name): + """ + Args: + name (str, int): + + Returns: + int + """ + if isinstance(name, int): + return name + else: + if name.isdigit(): + return int(name) + elif name in ['a', 'c', 'sp']: + return 1 + elif name in ['b', 'd']: + return 2 + + +def separate_name(name): + """ + Args: + name (str): Stage name in lowercase, such as 7-2, d3, sp3. + + Returns: + tuple[str]: Campaign_name and stage index in lowercase, Such as ['7', '2'], ['d', '3'], ['sp', '3']. + """ + if '-' in name: + return name.split('-') + elif name.startswith('sp'): + return 'sp', name[-1] + elif name[0] in 'abcdef': + return name[0], name[-1] + + +class CampaignOcr: + stage = {} + chapter = 0 + + def extract_campaign_name_image(self, image): + match_threshold = 0.95 + result = cv2.matchTemplate(np.array(image), TEMPLATE_STAGE_CLEAR.image, cv2.TM_CCOEFF_NORMED) + # np.sort(result.flatten())[-10:] + # array([0.8680386 , 0.8688129 , 0.8693155 , 0.86967576, 0.87012905, + # 0.8705039 , 0.99954903, 0.99983317, 0.99996626, 1. ], + # dtype=float32) + result = np.array(np.where(result > match_threshold)).T + if result is None or len(result) == 0: + logger.warning('No stage clear image found.') + + name_offset = (75, 9) + name_size = (60, 16) + name_letter = (255, 255, 255) + name_back = (102, 102, 102) + digits = [] + for point in result: + point = point[::-1] + button = tuple(np.append(point, point + TEMPLATE_STAGE_CLEAR.image.shape[:2])) + point = point + name_offset + name = image.crop(np.append(point, point + name_size)) + name = extract_letters(name, letter=name_letter, back=name_back) + + stage = self.extract_stage_name(name) + color = TEMPLATE_STAGE_CLEAR.color + digits.append(Button(area=area_offset(stage, point), color=color, button=button, name='stage')) + + # chapter, stage = self.name_separate(name) + # color = TEMPLATE_STAGE_CLEAR.color + # digits.append(Button(area=area_offset(chapter, point), color=color, button=button, name='chapter')) + # digits.append(Button(area=area_offset(stage, point), color=color, button=button, name='stage')) + + return digits + + @staticmethod + def extract_stage_name(image): + x_skip = 2 + interval = 5 + x_color = np.convolve(np.mean(image, axis=0), np.ones(interval), 'valid') / interval + x_list = np.where(x_color[x_skip:] > 235)[0] + if x_list is None or len(x_list) == 0: + logger.warning('No interval between digit and text.') + + return (0, 0, x_list[0] + 1 + x_skip, image.shape[0]) + + @staticmethod + def name_separate(image): + """ + Args: + image (np.ndarray): (height, width) + + Returns: + list[np.ndarray]: + """ + # Image.fromarray(image.astype('uint8'), mode='L').show() + x_skip = 2 + interval = 5 + x_color = np.convolve(np.mean(image, axis=0), np.ones(interval), 'valid') / interval + x_list = np.where(x_color[x_skip:] > 235)[0] + if x_list is None or len(x_list) == 0: + logger.warning('No interval between digit and text.') + image = image[:, :x_list[0] + 1 + x_skip] + + dash_color_range = (220 - 3, 220 + 3) + dash_height_index = 9 + mean = np.mean(image, axis=0) + # print(mean) + x_list = np.where( + (mean > dash_color_range[0]) \ + & (mean < dash_color_range[1]) \ + & (np.argmin(image, axis=0) == dash_height_index) + )[0] + if x_list is None or len(x_list) == 0: + logger.warning('No dash found between digits') + chapter = (0, 0, x_list[0] - 1, image.shape[0]) + stage = (x_list[-1] + 1, 0, image.shape[1], image.shape[0]) + + return chapter, stage + + def get_stage_name(self, image): + self.stage = {} + buttons = self.extract_campaign_name_image(image) + # ocr = Digit(buttons) + ocr = Ocr(buttons, lang='stage') + result = ocr.ocr(image) + result = [res.lower().replace('--', '-') for res in result] + + chapter = [separate_name(res)[0] for res in result] + counter = collections.Counter(chapter) + self.chapter = counter.most_common()[0][0] + + for name, button in zip(result, buttons): + button.area = button.button + button.name = name + self.stage[name] = button + + # self.chapter = np.argmax(np.bincount(result[::2])) + # for stage, button in zip(result[1::2], buttons[1::2]): + # button.area = button.button + # button.name = f'STAGE_{self.chapter}_{stage}' + # self.stage[f'{self.chapter}-{stage}'] = button + + logger.attr('Chapter', self.chapter) + logger.attr('Stage', ', '.join(self.stage.keys())) + + def get_chapter_index(self, image): + """ + A tricky method for ui_ensure_index + """ + self.get_stage_name(image) + + return ensure_chapter_index(self.chapter) diff --git a/module/campaign/campaign_ui.py b/module/campaign/campaign_ui.py new file mode 100644 index 000000000..cda970e7b --- /dev/null +++ b/module/campaign/campaign_ui.py @@ -0,0 +1,95 @@ +import numpy as np + +from module.base.utils import get_color +from module.campaign.assets import * +from module.campaign.campaign_ocr import CampaignOcr, ensure_chapter_index, separate_name +from module.logger import logger +from module.ui.ui import UI + +STAGE_SHOWN_WAIT = (1, 1.2) + + +class CampaignUI(UI): + campaign_ocr = CampaignOcr() + + def campaign_ensure_chapter(self, index): + """ + Args: + index (int, str): Chapter. Such as 7, 'd', 'sp'. + """ + index = ensure_chapter_index(index) + + # A tricky way to use ui_ensure_index. + self.ui_ensure_index(index, letter=self.campaign_ocr.get_chapter_index, + prev_button=CHAPTER_PREV, next_button=CHAPTER_NEXT, + fast=True, skip_first_screenshot=True, step_sleep=STAGE_SHOWN_WAIT, finish_sleep=0) + + def campaign_predict_mode(self): + """ + Returns: + str: 'normal' or 'hard' + """ + color = get_color(self.device.image, MODE_CHANGE.area) + if np.max(color) - np.min(color) < 50: + logger.warning(f'Unexpected color: {color}') + index = np.argmax(color) # R, G, B + if index == 0: + return 'normal' # Red button. (214, 117, 115) + elif index == 2: + return 'hard' # Blue button. (115, 146, 214) + else: + logger.warning(f'Unexpected color: {color}') + + def campaign_ensure_mode(self, mode='normal'): + """ + Args: + mode (str): 'normal', 'hard'. + + Returns: + bool: If mode changed. + """ + if self.campaign_predict_mode() == mode: + return False + else: + # Poor click. May unstable. + self.device.click(MODE_CHANGE) + self.handle_stage_icon_spawn() + return True + + def campaign_get_entrance(self, name): + """ + Args: + name (str): Campaign name, such as '7-2', 'd3', 'sp3'. + + Returns: + Button: + """ + if name not in self.campaign_ocr.stage: + logger.warning(f'Stage not found: {name}') + return self.campaign_ocr.stage[name] + + def ensure_campaign_ui(self, name, mode='normal'): + """ + Args: + name (str): Campaign name, such as '7-2', 'd3', 'sp3'. + mode (str): 'normal' or 'hard'. + """ + chapter, _ = separate_name(name) + + if chapter.isdigit(): + self.ui_weigh_anchor() + self.campaign_ensure_mode('normal') + self.campaign_ensure_chapter(index=chapter) + if mode == 'hard': + self.campaign_ensure_mode('hard') + + elif chapter in 'abcd': + self.ui_goto_event() + if chapter in 'ab': + self.campaign_ensure_mode('normal') + else: + self.campaign_ensure_mode('hard') + self.campaign_ensure_chapter(index=chapter) + + elif chapter == 'sp': + self.ui_goto_sp() diff --git a/module/campaign/run.py b/module/campaign/run.py new file mode 100644 index 000000000..a06d4fd84 --- /dev/null +++ b/module/campaign/run.py @@ -0,0 +1,123 @@ +import copy +import importlib +import os +from datetime import datetime + +from module.base.ocr import Digit +from module.campaign.assets import * +from module.campaign.campaign_base import CampaignBase +from module.campaign.campaign_ui import CampaignUI +from module.config.config import AzurLaneConfig +from module.logger import logger + +OCR_OIL = Digit(OCR_OIL, letter=(247, 247, 247), back=(33, 36, 49), limit=25000, name='OCR_OIL') + + +class CampaignRun(CampaignUI): + folder: str + name: str + stage: str + module = None + config: AzurLaneConfig + campaign: CampaignBase + + def load_campaign(self, name, folder='campaign_main'): + """ + Args: + name (str): Name of .py file under module.campaign. + folder (str): Name of the file folder under campaign. + + Returns: + bool: If load. + """ + if hasattr(self, 'name') and name == self.name: + return False + + self.name = name + self.folder = folder + + if folder.startswith('campaign_'): + self.stage = '-'.join(name.split('_')[1:3]) + if folder.startswith('event'): + self.stage = name + + self.module = importlib.import_module('.' + name, f'campaign.{folder}') + config = copy.copy(self.config).merge(self.module.Config()) + device = copy.copy(self.device) + device.config = config + self.campaign = self.module.Campaign(config=config, device=device) + self.campaign_name_set(name) + + # self.config = self.config.merge(self.module.Config()) + # self.campaign_name_set(name) + # self.device.config = self.config + # self.campaign = self.module.Campaign(config=self.config, device=self.device) + + return True + + def campaign_name_set(self, name): + # self.config.CAMPAIGN_NAME = name + # folder = self.config.SCREEN_SHOT_SAVE_FOLDER_BASE + '/' + name + # if not os.path.exists(folder): + # os.mkdir(folder) + # self.config.SCREEN_SHOT_SAVE_FOLDER = folder + + folder = self.campaign.config.SCREEN_SHOT_SAVE_FOLDER_BASE + '/' + name + if not os.path.exists(folder): + os.mkdir(folder) + self.campaign.config.SCREEN_SHOT_SAVE_FOLDER = folder + + def oil_check(self): + """ + Returns: + bool: If have enough oil. + """ + if not self.config.STOP_IF_OIL_LOWER_THAN: + return True + self.device.screenshot() + return OCR_OIL.ocr(self.device.image) > self.config.STOP_IF_OIL_LOWER_THAN + + def run(self, name, folder='campaign_main'): + """ + Args: + name (str): Name of .py file. + folder (str): Name of the file folder under campaign. + """ + self.load_campaign(name, folder=folder) + n = 0 + while 1: + # End + if n >= self.config.STOP_IF_COUNT_GREATER_THAN > 0: + logger.hr('Triggered count stop') + break + if not self.oil_check(): + logger.hr('Triggered oil limit') + break + if self.config.STOP_IF_TIME_REACH and datetime.now() > self.config.STOP_IF_TIME_REACH: + logger.hr('Triggered time limit') + self.config.config.set('Setting', 'if_time_reach', '0') + self.config.save() + break + if self.config.STOP_IF_TRIGGER_EMOTION_LIMIT and self.campaign.config.EMOTION_LIMIT_TRIGGERED: + logger.hr('Triggered emotion limit') + break + + # Log + logger.hr(name, level=1) + if self.config.STOP_IF_COUNT_GREATER_THAN > 0: + logger.info(f'Count: [{n}/{self.config.STOP_IF_COUNT_GREATER_THAN}]') + else: + logger.info(f'Count: [{n}]') + + # Run + self.ensure_campaign_ui(name=self.stage) + self.campaign.ENTRANCE = self.campaign_get_entrance(name=self.stage) + self.campaign.run() + + # After run + n += 1 + if self.config.STOP_IF_COUNT_GREATER_THAN > 0: + count = self.config.STOP_IF_COUNT_GREATER_THAN - n + count = 0 if count < 0 else count + self.config.config.set('Setting', 'if_count_greater_than', str(count)) + self.config.save() diff --git a/module/combat/assets.py b/module/combat/assets.py new file mode 100644 index 000000000..6d122e522 --- /dev/null +++ b/module/combat/assets.py @@ -0,0 +1,23 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +AUTOMATION_CONFIRM = Button(area=(553, 526, 727, 584), color=(98, 146, 206), button=(553, 526, 727, 584), file='./assets/combat/AUTOMATION_CONFIRM.png') +AUTOMATION_CONFIRM_CHECK = Button(area=(745, 423, 764, 439), color=(181, 85, 82), button=(745, 423, 764, 439), file='./assets/combat/AUTOMATION_CONFIRM_CHECK.png') +AUTOMATION_OFF = Button(area=(754, 113, 794, 129), color=(119, 71, 70), button=(754, 113, 794, 129), file='./assets/combat/AUTOMATION_OFF.png') +AUTOMATION_ON = Button(area=(759, 113, 790, 129), color=(22, 112, 81), button=(759, 113, 790, 129), file='./assets/combat/AUTOMATION_ON.png') +AUTOMATION_SWITCH = Button(area=(821, 106, 907, 136), color=(95, 109, 147), button=(821, 106, 907, 136), file='./assets/combat/AUTOMATION_SWITCH.png') +BATTLE_PREPARATION = Button(area=(1043, 607, 1241, 667), color=(234, 179, 97), button=(1043, 607, 1241, 667), file='./assets/combat/BATTLE_PREPARATION.png') +BATTLE_PREPARATION_WITH_OVERLAY = Button(area=(1058, 622, 1226, 652), color=(96, 74, 39), button=(1058, 622, 1226, 652), file='./assets/combat/BATTLE_PREPARATION_WITH_OVERLAY.png') +BATTLE_STATUS_A = Button(area=(622, 266, 732, 288), color=(235, 227, 111), button=(1000, 631, 1055, 689), file='./assets/combat/BATTLE_STATUS_A.png') +BATTLE_STATUS_S = Button(area=(633, 297, 722, 320), color=(233, 241, 127), button=(1000, 631, 1055, 689), file='./assets/combat/BATTLE_STATUS_S.png') +EXP_INFO_A = Button(area=(389, 100, 444, 116), color=(236, 231, 116), button=(1000, 631, 1055, 689), file='./assets/combat/EXP_INFO_A.png') +EXP_INFO_S = Button(area=(396, 122, 457, 137), color=(233, 241, 127), button=(1000, 631, 1055, 689), file='./assets/combat/EXP_INFO_S.png') +GET_ITEMS_1 = Button(area=(538, 217, 741, 253), color=(160, 192, 248), button=(1000, 631, 1055, 689), file='./assets/combat/GET_ITEMS_1.png') +GET_ITEMS_2 = Button(area=(538, 146, 742, 182), color=(160, 192, 248), button=(1000, 631, 1055, 689), file='./assets/combat/GET_ITEMS_2.png') +GET_MISSION = Button(area=(553, 482, 727, 539), color=(93, 142, 203), button=(553, 482, 727, 539), file='./assets/combat/GET_MISSION.png') +GET_SHIP = Button(area=(1104, 610, 1110, 630), color=(255, 255, 255), button=(1000, 631, 1055, 689), file='./assets/combat/GET_SHIP.png') +LOADING_BAR = Button(area=(33, 676, 1247, 680), color=(172, 205, 232), button=(33, 676, 1247, 680), file='./assets/combat/LOADING_BAR.png') +PAUSE = Button(area=(1236, 37, 1244, 59), color=(247, 243, 247), button=(1162, 34, 1246, 61), file='./assets/combat/PAUSE.png') diff --git a/module/combat/combat.py b/module/combat/combat.py new file mode 100644 index 000000000..df39b7602 --- /dev/null +++ b/module/combat/combat.py @@ -0,0 +1,271 @@ +from module.base.timer import Timer +from module.base.utils import color_bar_percentage +from module.combat.assets import * +from module.combat.emotion import Emotion +from module.combat.hp_balancer import HPBalancer +from module.handler.enemy_searching import EnemySearchingHandler +from module.handler.urgent_commission import UrgentCommissionHandler +from module.logger import logger +from module.map.assets import MAP_OFFENSIVE +from module.map.exception import CampaignEnd +from module.retire.retirement import Retirement +from module.ui.assets import BACK_ARROW + + +class Combat(HPBalancer, UrgentCommissionHandler, EnemySearchingHandler, Retirement): + _automation_set_timer = Timer(1) + _emotion: Emotion + + @property + def emotion(self): + if not hasattr(self, '_emotion'): + self._emotion = Emotion(config=self.config) + return self._emotion + + def combat_appear(self): + """ + Returns: + bool: If enter combat. + """ + if self.config.ENABLE_MAP_FLEET_LOCK and not self.is_in_map(): + if self.is_combat_loading(): + return True + if self.handle_retirement(): + self.map_offensive() + return True + + if self.appear(BATTLE_PREPARATION): + return True + if self.appear(BATTLE_PREPARATION_WITH_OVERLAY) and self.handle_combat_automation_confirm(): + return True + + return False + + def map_offensive(self): + while 1: + self.device.screenshot() + + if self.appear_then_click(MAP_OFFENSIVE, interval=1): + continue + + # Break + if self.combat_appear(): + break + + def is_combat_loading(self): + """ + Returns: + bool: + """ + left = color_bar_percentage(self.device.image, area=LOADING_BAR.area, prev_color=(99, 150, 255)) + right = color_bar_percentage(self.device.image, area=LOADING_BAR.area, prev_color=(225, 225, 225), reverse=True) + if 0.15 < left < 0.95 and right > 0.15 and left + right <= 1.2: + logger.attr('Loading', f'{int(left * 100)}%({int(right * 100)}%)') + return True + + return False + + def is_combat_executing(self): + """ + Returns: + bool: + """ + return self.appear(PAUSE) + + def handle_combat_automation_confirm(self): + if self.appear(AUTOMATION_CONFIRM_CHECK, interval=1): + self.appear_then_click(AUTOMATION_CONFIRM, offset=True) + return True + + return False + + def combat_preparation(self, balance_hp=False, emotion_reduce=False, auto=True, fleet_index=1): + """ + Args: + balance_hp (bool): + emotion_reduce (bool): + auto (bool): + fleet_index (int): + """ + logger.info('Combat preparation.') + + if emotion_reduce and not self.config.ENABLE_MAP_FLEET_LOCK: + self.emotion.wait() + if balance_hp: + self.balance_scout_hp() + # logger.info('start combat') + + while 1: + self.device.screenshot() + + # Automation. + if self.appear(BATTLE_PREPARATION): + # if self.handle_combat_automation_confirm(): + # continue + if self.handle_combat_automation_set(auto=auto): + continue + + # Retirement + if self.handle_retirement(): + continue + + # Combat start + if self.appear_then_click(BATTLE_PREPARATION): + continue + + if self.handle_combat_automation_confirm(): + continue + + # End + if self.is_combat_executing(): + if emotion_reduce: + self.emotion.reduce(fleet_index) + break + + def handle_combat_automation_set(self, auto): + """ + Args: + auto (bool): If use auto. + + Returns: + bool: + """ + if not self._automation_set_timer.reached(): + return False + + if self.appear(AUTOMATION_ON): + logger.info('[Automation] ON') + if not auto: + self.device.click(AUTOMATION_SWITCH) + self.device.sleep(1) + self._automation_set_timer.reset() + return True + + if self.appear(AUTOMATION_OFF): + logger.info('[Automation] OFF') + if auto: + self.device.click(AUTOMATION_SWITCH) + self.device.sleep(1) + self._automation_set_timer.reset() + return True + + if self.appear_then_click(AUTOMATION_CONFIRM, offset=True): + self._automation_set_timer.reset() + return True + + return False + + def combat_execute(self, func=None, call_submarine_at_boss=False, save_get_items=False): + """ + Args: + func: Funtion to run when in combat. + call_submarine_at_boss (bool): + save_get_items (bool) + """ + logger.info('Combat execute') + confirm_timer = Timer(10) + confirm_timer.start() + + while 1: + self.device.screenshot() + + if not confirm_timer.reached() and self.appear_then_click(AUTOMATION_CONFIRM, offset=True): + continue + + if call_submarine_at_boss: + pass + + # End + # if self.appear_then_click(BATTLE_STATUS): + # break + if self.handle_battle_status(save_get_items=save_get_items): + break + + def handle_battle_status(self, save_get_items): + """ + Args: + save_get_items (bool): + + Returns: + bool: + """ + if self.appear_then_click(BATTLE_STATUS_S, screenshot=save_get_items, genre='status'): + self.device.sleep((0.25, 0.5)) + return True + if self.appear_then_click(BATTLE_STATUS_A, screenshot=save_get_items, genre='status'): + self.device.sleep((0.25, 0.5)) + logger.warning('Battle status: A') + return True + + return False + + def combat_status(self, save_get_items=False, expected_end=None): + """ + Args: + save_get_items (bool): + expected_end (str): with_searching, no_searching, in_stage. + """ + logger.info('Combat status') + while 1: + self.device.screenshot() + + # Combat status + if self.appear_then_click(GET_ITEMS_1, screenshot=save_get_items, genre='get_items', offset=5): + continue + if self.appear_then_click(GET_ITEMS_2, screenshot=save_get_items, genre='get_items', offset=5): + continue + if self.handle_battle_status(save_get_items=save_get_items): + continue + if self.appear_then_click(GET_SHIP): + continue + if self.appear_then_click(EXP_INFO_S): + self.device.sleep((0.25, 0.5)) + continue + if self.appear_then_click(EXP_INFO_A): + self.device.sleep((0.25, 0.5)) + continue + if self.handle_urgent_commission(save_get_items=save_get_items): + continue + + # End + if expected_end is None: + if self.handle_in_stage() or self.handle_in_map_with_enemy_searching(): + break + if isinstance(expected_end, str): + if expected_end == 'in_stage' and self.handle_in_stage(): + raise CampaignEnd('Boss clear') + if expected_end == 'with_searching' and self.handle_in_map_with_enemy_searching(): + break + if expected_end == 'no_searching' and self.handle_in_map(): + break + if expected_end == 'in_ui' and self.appear(BACK_ARROW, offset=(20, 20)): + break + if callable(expected_end): + if expected_end(): + break + + def combat(self, banlance_hp=None, emotion_reduce=None, func=None, call_submarine_at_boss=None, save_get_items=None, + expected_end=None): + """ + Execute a combat. + """ + banlance_hp = banlance_hp if banlance_hp is not None else self.config.ENABLE_HP_BALANCE + emotion_reduce = emotion_reduce if emotion_reduce is not None else self.config.ENABLE_EMOTION_REDUCE + auto = func is None + call_submarine_at_boss = call_submarine_at_boss if call_submarine_at_boss is not None else self.config.SUBMARINE_CALL_AT_BOSS + save_get_items = save_get_items if save_get_items is not None else self.config.ENABLE_SAVE_GET_ITEMS + if expected_end == 'in_stage': + emotion_reduce = False + fleet_index = 2 + else: + fleet_index = 1 + + # if not hasattr(self, 'emotion'): + # self.emotion = Emotion(config=self.config) + + self.combat_preparation( + balance_hp=banlance_hp, emotion_reduce=emotion_reduce, auto=auto, fleet_index=fleet_index) + self.combat_execute( + func=func, call_submarine_at_boss=call_submarine_at_boss, save_get_items=save_get_items) + self.combat_status( + save_get_items=save_get_items, expected_end=expected_end) diff --git a/module/combat/emotion.py b/module/combat/emotion.py new file mode 100644 index 000000000..968f28b98 --- /dev/null +++ b/module/combat/emotion.py @@ -0,0 +1,97 @@ +from datetime import datetime +from time import sleep + +from module.config.config import AzurLaneConfig +from module.logger import logger + +config_name = 'EmotionRecord' + + +class Emotion: + def __init__(self, config): + """ + Args: + config (AzurLaneConfig): + """ + self.config = config + self.emotion = self.config.config + # self.load() + self.update() + self.record() + + # def load(self): + # logger.hr('Emotion load') + # self.emotion.read_file(codecs.open(self.config.EMOTION_LOG, "r", "utf8")) + # self.update() + + def record(self): + for index in [1, 2, 3]: + logger.attr(f'Emotion fleet_{index}', self.emotion[config_name][f'fleet_{index}_emotion']) + # self.emotion.write(codecs.open(self.config.CONFIG_FILE, "w+", "utf8")) + self.config.save() + + def recover_value(self, index): + return self.config.__getattribute__('FLEET_%s_RECOVER_PER_HOUR' % index) // 10 + + def emotion_limit(self, index): + return self.config.__getattribute__('FLEET_%s_EMOTION_LIMIT' % index) + + def recover_stop(self, index): + return 150 if self.recover_value(index) > 3 else 119 + + def update(self): + for index in [1, 2, 3]: + savetime = datetime.strptime(self.emotion[config_name][f'fleet_{index}_savetime'], self.config.TIME_FORMAT) + savetime = int(savetime.timestamp()) + recover_count = int(datetime.now().timestamp() // 360 - savetime // 360) + + value = self.emotion.getint(config_name, f'fleet_{index}_emotion') + + value += self.recover_value(index=index) * recover_count + if value > self.recover_stop(index=index): + value = self.recover_stop(index) + self.emotion[config_name][f'fleet_{index}_emotion'] = str(value) + self.emotion[config_name][f'fleet_{index}_savetime'] = str( + datetime.strftime(datetime.now(), self.config.TIME_FORMAT)) + + def reduce(self, index): + logger.hr('Emotion reduce') + self.update() + self.emotion[config_name][f'fleet_{index}_emotion'] = str(int( + self.emotion[config_name][f'fleet_{index}_emotion']) - 2) + self.record() + + def recovered_time(self, fleet=(1, 2)): + # fleet = [2, 3] if self.config.USING_SPARE_FLEET else [1, 2] + recover_count = [ + (self.emotion_limit(index) - int(self.emotion[config_name][f'fleet_{index}_emotion'])) \ + // self.recover_value(index) for index in fleet] + recover_count = max(recover_count) + recover_timestamp = datetime.now().timestamp() // 360 + recover_count + 1 + return datetime.fromtimestamp(recover_timestamp * 360) + + def emotion_triggered(self, fleet): + """ + Args: + fleet (int, list): + + Returns: + bool: + """ + if not isinstance(fleet, list): + fleet = [fleet] + return datetime.now() > self.recovered_time(fleet=fleet) + + def emotion_recovered(self, fleet): + pass + + def wait(self): + self.update() + recovered_time = self.recovered_time() + while 1: + if datetime.now() > recovered_time: + break + + logger.attr('Emotion recovered', recovered_time) + self.config.EMOTION_LIMIT_TRIGGERED = True + sleep(60) diff --git a/module/combat/hp_balancer.py b/module/combat/hp_balancer.py new file mode 100644 index 000000000..4236d9667 --- /dev/null +++ b/module/combat/hp_balancer.py @@ -0,0 +1,130 @@ +import numpy as np + +from module.base.base import ModuleBase +from module.base.button import color_similar +from module.logger import logger + +# Location of six HP bar. +LOCATION = [ + (36, 195), + (36, 295), + (36, 395), + (36, 497), + (36, 597), + (36, 697) +] +# HP bar size. +SIZE = (67, 4) +# Color that shows on HP bar. +COLOR_HP_GREEN = (156, 235, 57) +COLOR_HP_RED = (99, 44, 24) +# If difference greater than this, change position. +SCOUT_HP_DIFFERENCE_THRESHOLD = 0.2 +SCOUT_POSITION = [ + (403, 421), + (625, 369), + (821, 326) +] +# Give a normal distribution random offset when swiping to a point. +# (x_min, y_min, x_max, y_max). +POINT_OFFSET = (0, 0, 0, 0) +# Give a normal distribution random offset when moving up and down. +# (x_min, y_min, x_max, y_max). +UP_OFFSET = (0, 10, 0, 10) +DOWN_OFFSET = (0, -10, 0, -10) + + +class HPBalancer(ModuleBase): + hp = [] + _scout_order = (0, 1, 2) + + def _calculate_hp(self, location, size): + """Calculate hp according to color. + + Args: + location (tuple): Upper right of HP bar. (x, y) + size (tuple): Size of HP bar. (x, y) + + Returns: + float: HP. + """ + area = np.append(np.array(location), np.array(location) + np.array(size)) + data = self.image.crop(area).resize((size[0], 1)) + data = [ + color_similar(pixel, COLOR_HP_GREEN) or color_similar(pixel, COLOR_HP_RED) + for pixel in np.array(data)[0] + ] + data = np.sum(data) / size[0] + return data + + def get_hp(self): + """Get current HP from screenshot. + + Returns: + list: HP(float) of 6 ship. + """ + self.hp = [self._calculate_hp(loca, SIZE) for loca in LOCATION] + logger.info( + 'HP:' + ' '.join([str(int(data*100)).rjust(3)+'%' for data in self.hp]) + ) + return self.hp + + def scout_position_change(self, p1, p2): + """Exchange KAN-SEN's position. + It need to move up and down a little, even though it moves to the right location. + + Args: + p1 (int): Origin position [0, 2]. + p2 (int): Target position [0, 2]. + """ + logger.info('scout_position_change (%s, %s)' % (p1, p2)) + p1 = self.drag_node(SCOUT_POSITION[p1], POINT_OFFSET, 0.25) + p2 = self.drag_node(SCOUT_POSITION[p2], POINT_OFFSET, 0.1) + path = [ + p1, + p2, + self.drag_node(p2[:2], UP_OFFSET, 0.1), + self.drag_node(p2[:2], DOWN_OFFSET, 0.1), + p2 + ] + self.drag(path) + + @staticmethod + def _expected_scout_order(hp): + descending = np.sort(hp)[::-1] + sort = np.argsort(hp)[::-1] + + if descending[0] - descending[2] > SCOUT_HP_DIFFERENCE_THRESHOLD: + if descending[1] - descending[2] > SCOUT_HP_DIFFERENCE_THRESHOLD: + # 100% 70% 40% + order = [sort[0], sort[2], sort[1]] + else: + # 100% 70% 60% + order = [sort[0], 1, 2] + order[sort[0]] = 0 + else: + # 80% 80% 80% + order = [0, 1, 2] + return order + + @staticmethod + def _gen_exchange_step(origin, target): + diff = np.array(target) - np.array(origin) + count = np.count_nonzero(diff) + if count == 3: + yield (0, 2) + if np.argsort(target)[0] - np.argsort(origin)[0] == 1: + yield (1, 2) + else: + yield (0, 1) + elif count == 2: + yield tuple(np.nonzero(diff)[0]) + elif count == 0: + # Target is the same as origin. Do nothing + pass + + def balance_scout_hp(self): + target = self._expected_scout_order(self.hp[3:]) + for step in self._gen_exchange_step(self._scout_order, target): + self.scout_position_change(*step) + self.sleep(0.5) diff --git a/module/config/config.py b/module/config/config.py new file mode 100644 index 000000000..d617bc63d --- /dev/null +++ b/module/config/config.py @@ -0,0 +1,395 @@ +import codecs +import configparser +import copy +import os +from datetime import datetime, timedelta + +import numpy as np +from PIL import Image + +from module.config.dictionary import * +from module.logger import logger + + +class AzurLaneConfig: + """ + Basic Config. + """ + CONFIG_FILE = '' + config = configparser.ConfigParser(interpolation=None) + + """ + Fleet + """ + ENABLE_FLEET_CONTROL = True + # Fleet 1-6, if empty use 0. + _FLEET_1 = 1 + FLEET_2 = 2 + FLEET_3 = 3 + # Formation 1-3. + _FLEET_1_FORMATION = 2 + FLEET_2_FORMATION = 2 + FLEET_3_FORMATION = 2 + # Fleet 1-2, if empty use 0. + SUBMARINE = 0 + FLEET_CHECKED = False + + USING_SPARE_FLEET = False + + @property + def FLEET_1(self): + return self.FLEET_3 if self.USING_SPARE_FLEET else self._FLEET_1 + + @FLEET_1.setter + def FLEET_1(self, value): + self._FLEET_1 = value + + @property + def FLEET_1_FORMATION(self): + return self.FLEET_3_FORMATION if self.USING_SPARE_FLEET else self._FLEET_1_FORMATION + + @FLEET_1_FORMATION.setter + def FLEET_1_FORMATION(self, value): + self._FLEET_1_FORMATION = value + + """ + module.assets + """ + ASSETS_FOLDER = './assets' + + """ + module.base + """ + COLOR_SIMILAR_THRESHOLD = 10 + BUTTON_OFFSET = 30 + BUTTON_MATCH_SIMILARITY = 0.85 + SLEEP_AFTER_CLICK = 0 + WAIT_BEFORE_SAVING_SCREEN_SHOT = 1 + + """ + module.combat + """ + ENABLE_SAVE_GET_ITEMS = True + ENABLE_HP_BALANCE = False + ENABLE_MAP_FLEET_LOCK = True + SUBMARINE_MODE = '' + SUBMARINE_CALL_AT_BOSS = False + + """ + module.campaign + """ + CAMPAIGN_NAME = 'default' + CAMPAIGN_MODE = 'normal' + + ENABLE_STOP_CONDITION = True + STOP_IF_OIL_LOWER_THAN = 5000 + STOP_IF_COUNT_GREATER_THAN = 0 + STOP_IF_TIME_REACH = 0 + STOP_IF_TRIGGER_EMOTION_LIMIT = False + STOP_IF_DOCK_FULL = False + + """ + module.event + """ + CAMPAIGN_EVENT = '' + EVENT_NAME_AB = '' + + """ + module.combat.emotion + """ + ENABLE_EMOTION_REDUCE = True + IGNORE_LOW_EMOTION_WARN = False + EMOTION_LIMIT_TRIGGERED = False + TIME_FORMAT = '%Y-%m-%d_%H:%M:%S' + # 20, 30, 40, 50, 60 + FLEET_1_RECOVER_PER_HOUR = 50 + FLEET_1_EMOTION_LIMIT = 120 + FLEET_2_RECOVER_PER_HOUR = 20 + FLEET_2_EMOTION_LIMIT = 50 + FLEET_3_RECOVER_PER_HOUR = 20 + FLEET_3_EMOTION_LIMIT = 50 + + """ + module.device + """ + SERIAL = '127.0.0.1:62001' + COMMAND = '' + USE_ADB_SREENSHOT = False + USE_ADB_CONTROL = False + SCREEN_SHOT_SAVE_FOLDER_BASE = './screenshot' + SCREEN_SHOT_SAVE_FOLDER = '' + SCREEN_SHOT_SAVE_INTERVAL = 5 # Seconds between two save. Saves in the interval will be dropped. + + """ + module.daily + """ + ENABLE_DAILY_MISSION = True + FLEET_DAILY = 3 + FLEET_DAILY_EQUIPMENT = [1, 1, 1, 1, 1, 1] + DAILY_CHOOSE = { + 4: 1, # 商船护送 + 5: 1, # 海域突进 + 1: 2, # 战术研修, 1航空 2炮击 3雷击 + 2: 1, # 斩首行动 + 3: 1, # 破交作战 + } + + """ + module.hard + """ + ENABLE_HARD_CAMPAIGN = True + HARD_CAMPAIGN = '10-4' + FLEET_HARD = 1 + FLEET_HARD_EQUIPMENT = [1, 1, 1, 1, 1, 1] + + """ + module.exercise + """ + ENABLE_EXERCISE = True + EXERCISE_CHOOSE_MODE = 'max_exp' + EXERCISE_PRESERVE = 0 + LOW_HP_THRESHOLD = 0.40 + LOW_HP_CONFIRM_WAIT = 1.0 + OPPONENT_CHALLENGE_TRIAL = 1 + EXERCISE_FLEET_EQUIPMENT = [1, 1, 1, 1, 1, 1] + + """ + error_log + """ + ERROR_LOG_FOLDER = './log/error' + + """ + module.map.fleet + """ + MAP_HAS_AMBUSH = False + + """ + module.retire + """ + ENABLE_RETIREMENT = True + RETIRE_MODE = '10' # all, 10 + RETIRE_N = True + RETIRE_R = False + RETIRE_SR = False + RETIRE_SSR = False + + """ + module.map.perspective + """ + # Screen + SCREEN_SIZE = (1280, 720) + DETECTING_AREA = (123, 55, 1280, 720) + SCREEN_CENTER = (SCREEN_SIZE[0] / 2, SCREEN_SIZE[1] / 2) + MID_Y = SCREEN_CENTER[1] + # UI mask + UI_MASK_FILE = './module/map/ui_mask.png' + UI_MASK = np.array(Image.open(UI_MASK_FILE).convert('L')) + # Parameters for scipy.signal.find_peaks + # https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html + INTERNAL_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (150, 255 - 40), + 'width': 2, + 'prominence': 10, + 'distance': 35, + } + EDGE_LINES_FIND_PEAKS_PARAMETERS = { + 'height': (255 - 24, 255), + 'prominence': 10, + 'distance': 50, + 'wlen': 1000 + } + # Parameters for cv2.HoughLines + INTERNAL_LINES_HOUGHLINES_THRESHOLD = 75 + EDGE_LINES_HOUGHLINES_THRESHOLD = 75 + # Parameters for lines pre-cleansing + HORIZONTAL_LINES_THETA_THRESHOLD = 0.005 + VERTICAL_LINES_THETA_THRESHOLD = 18 + # Parameters for perspective calculating + VANISH_POINT_RANGE = ((540, 740), (-3000, -1000)) + DISTANCE_POINT_X_RANGE = ((-3200, -1600),) + # Parameters for line cleansing + ERROR_LINES_TOLERANCE = (-10, 10) + MID_DIFF_RANGE = (129 - 3, 129 + 3) + COINCIDENT_POINT_RANGE = ( + ( + -abs(ERROR_LINES_TOLERANCE[0]) * MID_DIFF_RANGE[1], + # SCREEN_SIZE[0] + ERROR_LINES_TOLERANCE[1] * MID_DIFF_RANGE[1] + 200 + ), + MID_DIFF_RANGE + ) + """ + module.daemon + """ + ENABLE_SEMI_MAP_PREPARATION = True + ENABLE_SEMI_STORY_SKIP = True + + """ + C_12_4_leveling + """ + C124_NON_S3_ENTER_TOLERANCE = 1 + C124_NON_S3_WITHDRAW_TOLERANCE = 0 + C124_AMMO_PICK_UP = 3 + + def create_folder(self): + self.SCREEN_SHOT_SAVE_FOLDER = self.SCREEN_SHOT_SAVE_FOLDER_BASE + '/' + self.CAMPAIGN_NAME + for folder in [self.SCREEN_SHOT_SAVE_FOLDER_BASE, self.ASSETS_FOLDER, self.SCREEN_SHOT_SAVE_FOLDER, self.ERROR_LOG_FOLDER]: + if not os.path.exists(folder): + os.mkdir(folder) + + def merge(self, other): + """ + Args: + other (AzurLaneConfig, Config): + + Returns: + AzurLaneConfig + """ + config = copy.copy(self) + for attr in dir(config): + if attr.endswith('__'): + continue + if hasattr(other, attr): + value = other.__getattribute__(attr) + if value is not None: + config.__setattr__(attr, value) + + return config + + def load_config_file(self, name='main'): + self.CONFIG_FILE = f'./config/{name}.ini' + self.config.read_file(codecs.open(self.CONFIG_FILE, "r", "utf8")) + self.load_from_config(self.config) + + def save(self): + self.config.write(codecs.open(self.CONFIG_FILE, "w+", "utf8")) + + def load_from_config(self, config): + """ + Args: + config(configparser.ConfigParser): + """ + self.COMMAND = config.get('Command', 'command') + + option = config['Setting'] + # Serial + self.SERIAL = option['serial'] + # Stop condition + self.ENABLE_STOP_CONDITION = to_bool(option['enable_stop_condition']) + self.STOP_IF_COUNT_GREATER_THAN = int(option['if_count_greater_than']) + if not option['if_time_reach'].isdigit(): + hour, minute = [int(x) for x in option['if_time_reach'].split(':')] + limit = datetime.now().replace(hour=hour, minute=minute, second=0, microsecond=0) + limit = limit + timedelta(1) if limit < datetime.now() else limit + self.STOP_IF_TIME_REACH = limit + else: + self.STOP_IF_TIME_REACH = 0 + self.STOP_IF_OIL_LOWER_THAN = int(option['if_oil_lower_than']) + self.STOP_IF_TRIGGER_EMOTION_LIMIT = to_bool(option['if_trigger_emotion_control']) + self.STOP_IF_DOCK_FULL = to_bool(option['if_dock_full']) + # Fleet + self.ENABLE_FLEET_CONTROL = to_bool(option['enable_fleet_control']) + for n in ['1', '2', '3']: + self.__setattr__(f'FLEET_{n}', int(option[f'fleet_index_{n}'])) + self.__setattr__(f'FLEET_{n}_FORMATION', int(option[f'fleet_formation_{n}'].split('_')[1])) + self.SUBMARINE = int(option['fleet_index_4']) if to_bool(option['fleet_index_4']) else 0 + self.SUBMARINE_MODE = option['submarine_mode'] + self.SUBMARINE_CALL_AT_BOSS = option['submarine_mode'] == 'when_boss_combat_boss_appear' + # Emotion + self.ENABLE_EMOTION_REDUCE = to_bool(option['enable_emotion_reduce']) + self.IGNORE_LOW_EMOTION_WARN = to_bool(option['ignore_low_emotion_warn']) + for n in ['1', '2', '3']: + recover = dic_emotion_recover[option[f'emotion_recover_{n}']] + recover += 10 if to_bool(option[f'hole_fleet_married_{n}']) else 0 + self.__setattr__(f'FLEET_{n}_RECOVER_PER_HOUR', recover) + self.__setattr__(f'FLEET_{n}_EMOTION_LIMIT', dic_emotion_limit[option[f'emotion_control_{n}']]) + # HP balance, save get items -> combat + self.ENABLE_HP_BALANCE = to_bool(option['enable_hp_balance']) + self.ENABLE_SAVE_GET_ITEMS = to_bool(option['enable_drop_screenshot']) + self.SCREEN_SHOT_SAVE_FOLDER_BASE = option['drop_screenshot_folder'] + # Retirement + self.ENABLE_RETIREMENT = to_bool(option['enable_retirement']) + self.RETIRE_MODE = option['retire_mode'].split('_')[1] + for r in ['n', 'r', 'sr', 'ssr']: + self.__setattr__(f'RETIRE_{r.upper()}', to_bool(option[f'retire_{r}'])) + + option = config['Main'] + self.CAMPAIGN_NAME = option['main_stage'] + self.CAMPAIGN_NAME = 'campaign_' + self.CAMPAIGN_NAME.replace('-', '_') + + option = config['Daily'] + for n in ['daily_mission', 'hard_campaign', 'exercise']: + self.__setattr__(f'ENABLE_{n.upper()}', option[f'enable_{n}']) + # Daily mission + self.ENABLE_DAILY_MISSION = to_bool(option['enable_daily_mission']) + for n in [1, 2, 4, 5]: + self.DAILY_CHOOSE[n] = dic_daily[option[f'daily_mission_{n}']] + self.FLEET_DAILY = int(option['daily_fleet']) + self.FLEET_DAILY_EQUIPMENT = equip(option['daily_equipment']) + # Hard + self.ENABLE_HARD_CAMPAIGN = to_bool(option['enable_hard_campaign']) + self.HARD_CAMPAIGN = option['hard_campaign'] + self.FLEET_HARD = int(option['hard_fleet']) + self.FLEET_HARD_EQUIPMENT = equip(option['hard_equipment']) + # Exercise + self.ENABLE_EXERCISE = to_bool(option['enable_exercise']) + self.EXERCISE_CHOOSE_MODE = option['exercise_choose_mode'] + self.EXERCISE_PRESERVE = int(option['exercise_preserve']) + self.OPPONENT_CHALLENGE_TRIAL = int(option['exercise_try']) + self.LOW_HP_THRESHOLD = float(option['exercise_hp_threshold']) + self.LOW_HP_CONFIRM_WAIT = float(option['exercise_low_hp_confirm']) + self.EXERCISE_FLEET_EQUIPMENT = equip(option['exercise_equipment']) + + # Event + option = config['Event'] + self.CAMPAIGN_EVENT = option['event_stage'] + + # Event_daily_ab + option = config['Event_daily_ab'] + self.EVENT_NAME_AB = option['event_name_ab'] + + # Semi_auto + option = config['Semi_auto'] + self.ENABLE_SEMI_MAP_PREPARATION = to_bool(option['enable_semi_map_preparation']) + self.ENABLE_SEMI_STORY_SKIP = to_bool(option['enable_semi_story_skip']) + + # C_12_4_leveling + option = config['C124_leveling'] + self.C124_NON_S3_ENTER_TOLERANCE = int(option['non_s3_enemy_enter_tolerance']) + self.C124_NON_S3_WITHDRAW_TOLERANCE = int(option['non_s3_enemy_withdraw_tolerance']) + self.C124_AMMO_PICK_UP = int(option['ammo_pick_up_124']) + + def record_executed_since(self, option, since): + """ + Args: + option (tuple(str)): (Section, Option), such as ('DailyRecord', 'exercise'). + since (tuple(int)): Update hour in Azurlane, such as (0, 12, 18,). + + Returns: + bool: If got a record after last game update. + """ + record = datetime.strptime(self.config.get(*option), self.TIME_FORMAT) + since = np.array(since) + + hour = since[since <= datetime.now().hour][-1] + update = datetime.now().replace(hour=hour, minute=0, second=0, microsecond=0) + + logger.attr(f'{option[0]}_{option[1]}', f'Record time: {record}') + logger.attr(f'{option[0]}_{option[1]}', f'Last update: {update}') + return record > update + + def record_save(self, option): + record = datetime.strftime(datetime.now(), self.TIME_FORMAT) + self.config.set(option[0], option[1], record) + self.save() + + def __init__(self, conf='main'): + """ + Args: + conf (str): Config to load. + """ + self.load_config_file(conf) + + self.create_folder() + +# cfg = AzurLaneConfig() diff --git a/module/config/dictionary.py b/module/config/dictionary.py new file mode 100644 index 000000000..66a39cd1b --- /dev/null +++ b/module/config/dictionary.py @@ -0,0 +1,150 @@ +dic_bool = { + 'yes': True, + 'no': False, + 'do_not_use': False, +} +dic_emotion_limit = { + 'keep_high_emotion': 120, + 'avoid_green_face': 40, + 'avoid_yellow_face': 30, + 'avoid_red_face': 2, +} +dic_emotion_recover = { + 'not_in_dormitory': 20, + 'dormitory_floor_1': 40, + 'dormitory_floor_2': 50, +} +dic_daily = { + 'daily_air': 1, + 'daily_gun': 2, + 'daily_torpedo': 3, + 'lv_70': 1, + 'lv_50': 2, + 'lv_35': 3, +} +dic_chi_to_eng = { + # Function + '出击设置': 'setting', + '每日任务': 'daily', + '主线图': 'main', + '活动图': 'event', + '活动AB图每日三倍': 'event_daily_ab', + '半自动': 'semi_auto', + '7-2三战拣垃圾': 'c72_mystery_farming', + '12-4打大型练级': 'c124_leveling', + + # Argument + '启用停止条件': 'enable_stop_condition', + '如果出击次数大于': 'if_count_greater_than', + '如果时间超过': 'if_time_reach', + '如果石油低于': 'if_oil_lower_than', + '如果触发心情控制': 'if_trigger_emotion_control', + '如果船舱已满': 'if_dock_full', + '启用舰队控制': 'enable_fleet_control', + '舰队编号1': 'fleet_index_1', + '舰队阵型1': 'fleet_formation_1', + '舰队编号2': 'fleet_index_2', + '舰队阵型2': 'fleet_formation_2', + '舰队编号3': 'fleet_index_3', + '舰队阵型3': 'fleet_formation_3', + '舰队编号4': 'fleet_index_4', + '潜艇出击方案': 'submarine_mode', + '启用心情消耗': 'enable_emotion_reduce', + '无视红脸出击警告': 'ignore_low_emotion_warn', + '心情回复1': 'emotion_recover_1', + '心情控制1': 'emotion_control_1', + '全员已婚1': 'hole_fleet_married_1', + '心情回复2': 'emotion_recover_2', + '心情控制2': 'emotion_control_2', + '全员已婚2': 'hole_fleet_married_2', + '心情回复3': 'emotion_recover_3', + '心情控制3': 'emotion_control_3', + '全员已婚3': 'hole_fleet_married_3', + '启用血量平衡': 'enable_hp_balance', + '启用退役': 'enable_retirement', + '退役方案': 'retire_mode', + '退役白皮': 'retire_n', + '退役蓝皮': 'retire_r', + '退役紫皮': 'retire_sr', + '退役金皮': 'retire_ssr', + '启用掉落记录': 'enable_drop_screenshot', + '掉落保存目录': 'drop_screenshot_folder', + '设备': 'serial', + '打每日': 'enable_daily_mission', + '打困难': 'enable_hard_campaign', + '打演习': 'enable_exercise', + '战术研修': 'daily_mission_1', + '斩首行动': 'daily_mission_2', + '商船护航': 'daily_mission_4', + '海域突进': 'daily_mission_5', + '每日舰队': 'daily_fleet', + '每日舰队快速换装': 'daily_equipment', + '困难地图': 'hard_campaign', + '困难舰队': 'hard_fleet', + '困难舰队快速换装': 'hard_equipment', + '演习对手选择': 'exercise_choose_mode', + '演习次数保留': 'exercise_preserve', + '演习尝试次数': 'exercise_try', + '演习SL阈值': 'exercise_hp_threshold', + '演习低血量确认时长': 'exercise_low_hp_confirm', + '演习快速换装': 'exercise_equipment', + '主线地图出击': 'main_stage', + '活动关卡出击': 'event_stage', + '活动名称ab': 'event_name_ab', + '进图准备': 'enable_semi_map_preparation', + '跳过剧情': 'enable_semi_story_skip', + '非大型敌人进图忍耐': 'non_s3_enemy_enter_tolerance', + '非大型敌人撤退忍耐': 'non_s3_enemy_withdraw_tolerance', + '拣弹药124': 'ammo_pick_up_124', + + # Option + '是': 'yes', + '否': 'no', + '单纵阵': 'formation_1', + '复纵阵': 'formation_2', + '轮形阵': 'formation_3', + '不使用': 'do_not_use', + '仅狩猎': 'hunt_only', + '每战出击': 'every_combat', + '空弹出击': 'when_no_ammo', + 'BOSS战出击': 'when_boss_combat', + 'BOSS战BOSS出现后召唤': 'when_boss_combat_boss_appear', + '未放置于后宅': 'not_in_dormitory', + '后宅一楼': 'dormitory_floor_1', + '后宅二楼': 'dormitory_floor_2', + '保持经验加成': 'keep_high_emotion', + '防止绿脸': 'avoid_green_face', + '防止黄脸': 'avoid_yellow_face', + '防止红脸': 'avoid_red_face', + '退役全部': 'retire_all', + '退役10个': 'retire_10', + '航空': 'daily_air', + '炮击': 'daily_gun', + '雷击': 'daily_torpedo', + '70级': 'lv_70', + '50级': 'lv_50', + '35级': 'lv_35', + '经验最多': 'max_exp', + '排名最前': 'max_ranking', + '福利队': 'good_opponent', + + # Event + '北境序曲': 'event_20200227_cn', + '复刻斯图尔特的硝烟': 'event_20200312_cn', + '微层混合': 'event_20200326_cn', + +} +dic_eng_to_chi = {v: k for k, v in dic_chi_to_eng.items()} + + +def to_bool(string): + return dic_bool.get(string, string) + + +def equip(string): + if string.isdigit(): + return None + + out = [int(letter.strip()) for letter in string.split(',')] + assert len(out) == 6 + return out diff --git a/module/daemon/assets.py b/module/daemon/assets.py new file mode 100644 index 000000000..a443d36cd --- /dev/null +++ b/module/daemon/assets.py @@ -0,0 +1,17 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +AMBUSH_AVOID = Button(area=(753, 444, 927, 502), color=(87, 129, 189), button=(979, 444, 1152, 502), file='./assets/daemon/AMBUSH_AVOID.png') +AT_SEA = Button(area=(749, 654, 921, 707), color=(213, 124, 124), button=(749, 654, 921, 707), file='./assets/daemon/AT_SEA.png') +FLEET_PREPARATION = Button(area=(981, 575, 1180, 636), color=(235, 185, 114), button=(981, 575, 1180, 636), file='./assets/daemon/FLEET_PREPARATION.png') +GET_EMERGENCY_REPAIR = Button(area=(645, 352, 666, 360), color=(255, 255, 255), button=(645, 352, 666, 360), file='./assets/daemon/GET_EMERGENCY_REPAIR.png') +GET_ITEMS = Button(area=(538, 217, 741, 253), color=(160, 192, 248), button=(1120, 643, 1146, 666), file='./assets/daemon/GET_ITEMS.png') +MAP_PREPARATION = Button(area=(853, 488, 1053, 549), color=(235, 186, 114), button=(853, 488, 1053, 549), file='./assets/daemon/MAP_PREPARATION.png') +STORY_CHOOCE = Button(area=(902, 344, 959, 357), color=(98, 122, 156), button=(902, 344, 959, 357), file='./assets/daemon/STORY_CHOOCE.png') +STORY_CHOOCE_2 = Button(area=(903, 388, 959, 399), color=(98, 121, 156), button=(903, 388, 959, 399), file='./assets/daemon/STORY_CHOOCE_2.png') +STORY_SKIP = Button(area=(1216, 676, 1258, 708), color=(148, 159, 178), button=(1180, 30, 1256, 49), file='./assets/daemon/STORY_SKIP.png') +STORY_SKIP_CONFIRM = Button(area=(703, 493, 877, 550), color=(93, 142, 201), button=(703, 493, 877, 550), file='./assets/daemon/STORY_SKIP_CONFIRM.png') +STRATEGY_OPEN = Button(area=(1102, 480, 1178, 482), color=(255, 223, 74), button=(1064, 405, 1093, 483), file='./assets/daemon/STRATEGY_OPEN.png') diff --git a/module/daemon/daemon.py b/module/daemon/daemon.py new file mode 100644 index 000000000..afe1b58b9 --- /dev/null +++ b/module/daemon/daemon.py @@ -0,0 +1,68 @@ +from module.combat.combat import Combat +from module.daemon.assets import * +from module.handler.ambush import MAP_AMBUSH_EVADE +from module.handler.enemy_searching import EnemySearchingHandler +from module.handler.mystery import MysteryHandler +from module.handler.urgent_commission import UrgentCommissionHandler +from module.map.map_fleet_preparation import FleetPreparation + + +class AzurLaneDaemon(FleetPreparation, Combat, UrgentCommissionHandler, EnemySearchingHandler, MysteryHandler): + def daemon(self): + + while 1: + self.device.screenshot() + + # If is running a combat, do nothing. + if self.is_combat_executing(): + continue + + # Combat + if self.combat_appear(): + # if self.handle_combat_automation_set(auto=True): + # continue + # self.device.click(BATTLE_PREPARATION) + self.combat_preparation() + if self.handle_battle_status(save_get_items=False): + self.combat_status(save_get_items=False, expected_end='no_searching') + continue + + # Map operation + if self.appear_then_click(MAP_AMBUSH_EVADE): + self.device.sleep(1) + continue + if self.appear_then_click(STRATEGY_OPEN): + continue + + # Map preparation + if self.config.ENABLE_SEMI_MAP_PREPARATION: + if self.appear_then_click(MAP_PREPARATION, interval=2): + continue + if self.appear_then_click(FLEET_PREPARATION, interval=2): + continue + + # Retire + pass + + # Emotion + pass + + # Urgent commission + if self.handle_urgent_commission(save_get_items=False): + continue + + # Story + if self.config.ENABLE_SEMI_STORY_SKIP: + if self.appear_then_click(STORY_SKIP, offset=True, interval=2): + continue + if self.appear_then_click(STORY_SKIP_CONFIRM, offset=True, interval=2): + continue + if self.appear_then_click(STORY_CHOOCE, offset=True, interval=2): + continue + if self.appear_then_click(STORY_CHOOCE_2, offset=True, interval=2): + continue + + # End + # No end condition, stop it manually. + + return True diff --git a/module/daily/assets.py b/module/daily/assets.py new file mode 100644 index 000000000..45a6f1382 --- /dev/null +++ b/module/daily/assets.py @@ -0,0 +1,18 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +DAILY_ACTIVE = Button(area=(611, 385, 711, 405), color=(5, 10, 7), button=(611, 385, 711, 405), file='./assets/daily/DAILY_ACTIVE.png') +DAILY_ENTER = Button(area=(534, 118, 744, 645), color=(136, 166, 185), button=(534, 118, 744, 645), file='./assets/daily/DAILY_ENTER.png') +DAILY_ENTER_CHECK = Button(area=(1222, 141, 1229, 150), color=(247, 209, 66), button=(1222, 141, 1229, 150), file='./assets/daily/DAILY_ENTER_CHECK.png') +DAILY_FLEET_NEXT = Button(area=(914, 325, 931, 354), color=(86, 115, 149), button=(914, 325, 931, 354), file='./assets/daily/DAILY_FLEET_NEXT.png') +DAILY_FLEET_PREV = Button(area=(36, 325, 53, 354), color=(103, 130, 163), button=(36, 325, 53, 354), file='./assets/daily/DAILY_FLEET_PREV.png') +DAILY_MISSION_1 = Button(area=(482, 140, 701, 255), color=(58, 80, 123), button=(482, 140, 701, 255), file='./assets/daily/DAILY_MISSION_1.png') +DAILY_MISSION_2 = Button(area=(482, 300, 701, 415), color=(61, 84, 128), button=(482, 300, 701, 415), file='./assets/daily/DAILY_MISSION_2.png') +DAILY_MISSION_3 = Button(area=(482, 460, 701, 575), color=(75, 106, 146), button=(482, 460, 701, 575), file='./assets/daily/DAILY_MISSION_3.png') +DAILY_NEXT = Button(area=(821, 161, 996, 604), color=(71, 48, 43), button=(821, 161, 996, 604), file='./assets/daily/DAILY_NEXT.png') +DAILY_PREV = Button(area=(282, 163, 456, 600), color=(60, 73, 71), button=(282, 163, 456, 600), file='./assets/daily/DAILY_PREV.png') +OCR_DAILY_FLEET_INDEX = Button(area=(524, 615, 562, 658), color=(66, 102, 158), button=(524, 615, 562, 658), file='./assets/daily/OCR_DAILY_FLEET_INDEX.png') +OCR_REMAIN = Button(area=(532, 119, 545, 141), color=(123, 151, 175), button=(532, 119, 545, 141), file='./assets/daily/OCR_REMAIN.png') diff --git a/module/daily/daily.py b/module/daily/daily.py new file mode 100644 index 000000000..fd0e61a12 --- /dev/null +++ b/module/daily/daily.py @@ -0,0 +1,131 @@ +import numpy as np + +from module.base.ocr import Digit +from module.base.utils import get_color +from module.combat.combat import Combat +from module.daily.assets import * +from module.equipment.fleet_equipment import DailyEquipment +from module.logger import logger +from module.ui.ui import page_daily, page_campaign, BACK_ARROW, DAILY_CHECK + +DAILY_MISSION_LIST = [DAILY_MISSION_1, DAILY_MISSION_2, DAILY_MISSION_3] +OCR_REMAIN = Digit(OCR_REMAIN, letter=(255, 255, 255), back=(127, 127, 127), length=1, white_list='0123') +OCR_DAILY_FLEET_INDEX = Digit(OCR_DAILY_FLEET_INDEX, letter=(90, 154, 255), back=(24, 32, 49), length=1, + white_list='123456') +RECORD_OPTION = ('DailyRecord', 'daily') +RECORD_SINCE = (0,) + + +class Daily(Combat, DailyEquipment): + daily_current: int + daily_checked: list + + def is_active(self): + color = get_color(image=self.device.image, area=DAILY_ACTIVE.area) + color = np.array(color).astype(float) + color = (np.max(color) + np.min(color)) / 2 + active = color > 30 + if active: + logger.attr(f'Daily_{self.daily_current}', 'active') + else: + logger.attr(f'Daily_{self.daily_current}', 'inactive') + return active + + def _wait_daily_switch(self): + self.device.sleep((1, 1.2)) + + def next(self): + self.daily_current += 1 + logger.info('Switch to %s' % str(self.daily_current)) + self.device.click(DAILY_NEXT) + self._wait_daily_switch() + self.device.screenshot() + + def prev(self): + self.daily_current -= 1 + logger.info('Switch to %s' % str(self.daily_current)) + self.device.click(DAILY_PREV) + self._wait_daily_switch() + self.device.screenshot() + + def daily_execute(self, remain): + logger.hr(f'Daily {self.daily_current}') + self.ui_click(click_button=DAILY_ENTER, check_button=DAILY_ENTER_CHECK, appear_button=DAILY_CHECK) + + def daily_end(): + return self.appear(DAILY_ENTER_CHECK) or self.appear(BACK_ARROW) + + button = DAILY_MISSION_LIST[self.config.DAILY_CHOOSE[self.daily_current] - 1] + for n in range(remain): + logger.hr(f'Count {n + 1}') + self.ui_click(click_button=button, check_button=self.combat_appear, appear_button=DAILY_ENTER_CHECK) + self.ui_ensure_index(self.config.FLEET_DAILY, letter=OCR_DAILY_FLEET_INDEX, prev_button=DAILY_FLEET_PREV, + next_button=DAILY_FLEET_NEXT, fast=False, skip_first_screenshot=True) + self.combat(emotion_reduce=False, save_get_items=False, expected_end=daily_end) + + self.ui_click(click_button=BACK_ARROW, check_button=DAILY_CHECK) + self.device.sleep((1, 1.2)) + + def daily_check(self, n=None): + if not n: + n = self.daily_current + self.daily_checked.append(n) + logger.info(f'Checked daily {n}') + logger.info(f'Checked_list: {self.daily_checked}') + + def daily_run_one(self): + self.ui_ensure(page_daily) + self.device.sleep(0.2) + self.device.screenshot() + self.daily_current = 1 + + logger.info(f'Checked_list: {self.daily_checked}') + for _ in range(max(self.daily_checked)): + self.next() + + while 1: + if self.daily_current > 5: + break + if self.daily_current == 3: + logger.info('Skip submarine daily.') + self.daily_check() + self.next() + continue + if not self.is_active(): + self.daily_check() + self.next() + continue + remain = OCR_REMAIN.ocr(self.device.image) + if remain == 0: + self.daily_check() + self.next() + continue + else: + self.daily_execute(remain=remain) + self.daily_check() + # The order of daily tasks will be disordered after execute a daily, exit and re-enter to reset. + # 打完一次之后每日任务的顺序会乱掉, 退出再进入来重置顺序. + self.ui_ensure(page_campaign) + break + + def daily_run(self): + self.daily_checked = [0] + + while 1: + self.daily_run_one() + + if max(self.daily_checked) >= 5: + logger.info('Daily clear complete.') + break + + def run(self): + self.equipment_take_on() + self.daily_run() + self.equipment_take_off() + self.ui_goto_main() + + def record_executed_since(self): + return self.config.record_executed_since(option=RECORD_OPTION, since=RECORD_SINCE) + + def record_save(self): + return self.config.record_save(option=RECORD_OPTION) diff --git a/module/device/connection.py b/module/device/connection.py new file mode 100644 index 000000000..144b67d45 --- /dev/null +++ b/module/device/connection.py @@ -0,0 +1,62 @@ +import subprocess + +import requests +import uiautomator2 as u2 + +from module.config.config import AzurLaneConfig +from module.logger import logger + + +class Connection: + def __init__(self, config): + """ + Args: + config(AzurLaneConfig): + """ + logger.hr('Device') + self.config = config + self.serial = str(self.config.SERIAL) + self.device = self.connect(self.serial) + self.disable_uiautomator2_auto_quit() + + @staticmethod + def adb_command(cmd, serial=None): + if serial: + cmd = ['adb', '-s', serial] + cmd + else: + cmd = ['adb'] + cmd + # process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # result = process.stdout.read() + # print(cmd) + result = subprocess.check_output(cmd, timeout=4, stderr=subprocess.STDOUT) + return result + + def adb_shell(self, cmd, serial=None): + cmd.insert(0, 'shell') + return self.adb_command(cmd, serial) + + def _adb_connect(self, serial): + if serial.startswith('127.0.0.1'): + msg = self.adb_command(['connect', serial]).decode("utf-8") + if msg.startswith('unable'): + logger.error('Unable to connect %s' % serial) + exit(1) + else: + logger.info(msg.strip()) + + def connect(self, serial): + """Connect to a device. + + Args: + serial (str): device serial or device address. + + Returns: + uiautomator2.UIAutomatorServer: Device. + """ + self._adb_connect(serial) + device = u2.connect(serial) + return device + + def disable_uiautomator2_auto_quit(self, port=7912, expire=300000): + self.adb_command(['forward', 'tcp:%s' % port, 'tcp:%s' % port], serial=self.serial) + requests.post('http://127.0.0.1:%s/newCommandTimeout' % port, data=str(expire)) diff --git a/module/device/control.py b/module/device/control.py new file mode 100644 index 000000000..dc8de61f0 --- /dev/null +++ b/module/device/control.py @@ -0,0 +1,161 @@ +import time + +from retrying import retry + +from module.base.timer import Timer +from module.base.utils import * +from module.device.connection import Connection +from module.logger import logger + + +class Control(Connection): + @staticmethod + def sleep(second): + """ + Args: + second(int, float, tuple): + """ + time.sleep(ensure_time(second)) + + @property + def time(self): + return time.time() + + @staticmethod + def _point2str(x, y): + return '(%s,%s)' % (str(int(x)).rjust(4), str(int(y)).rjust(4)) + + def click(self, button, adb=False): + """Method to click a button. + + Args: + button (button.Button): AzurLane Button instance. + adb (bool): If use adb. + """ + x, y = random_rectangle_point(button.button) + logger.info( + 'Click %s @ %s' % (self._point2str(x, y), button) + ) + if adb: + self._click_adb(x, y) + else: + self._click_uiautomator2(x, y) + self.sleep(self.config.SLEEP_AFTER_CLICK) + + @retry() + def _click_uiautomator2(self, x, y): + self.device.click(x, y) + + @retry() + def _click_adb(self, x, y): + self.adb_shell(['input', 'tap', str(x), str(y)], serial=self.serial) + + def multi_click(self, button, n, interval=(0.1, 0.2), adb=False): + click_timer = Timer(0.1) + for _ in range(n): + remain = ensure_time(interval) - click_timer.current() + if remain > 0: + self.sleep(remain) + + click_timer.reset() + self.click(button, adb=adb) + + def long_click(self, button, duration=(1, 1.2)): + """Method to long click a button. + + Args: + button (button.Button): AzurLane Button instance. + duration(int, float, tuple): + """ + x, y = random_rectangle_point(button.button) + duration = ensure_time(duration) + logger.info( + 'Click %s @ %s, %s' % (self._point2str(x, y), button, duration) + ) + self.device.long_click(x, y, duration=duration) + + def swipe(self, vector, box=(123, 159, 1193, 628), random_range=(0, 0, 0, 0), padding=15, duration=(0.1, 0.2)): + """Method to swipe. + + Args: + box(tuple): Swipe in box (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y). + vector(tuple): (x, y). + random_range(tuple): (x_min, y_min, x_max, y_max). + padding(int): + duration(int, float, tuple): + """ + duration = ensure_time(duration) + vector = np.array(vector) + random_rectangle_point(random_range) + vector = np.round(vector).astype(np.int) + half_vector = np.round(vector / 2).astype(np.int) + box = np.array(box) + np.append(np.abs(half_vector) + padding, -np.abs(half_vector) - padding) + center = random_rectangle_point(box) + start_point = center - half_vector + end_point = start_point + vector + logger.info( + 'Swipe %s -> %s, %s' % (self._point2str(*start_point), self._point2str(*end_point), duration) + ) + fx, fy, tx, ty = np.append(start_point, end_point).tolist() + if fx < 0 or tx < 0: + logger.warning('Swipe Error. vector: %s, box: %s, center: %s, half_vector: %s' % ( + str(vector), str(box), str(center), str(half_vector) + )) + self.device.swipe(fx, fy, tx, ty, duration=duration) + + def drag(self, path): + """Swipe following path. + + Args: + path (list): (x, y, sleep) + + Examples: + al.swipe([ + (403, 421, 0.2), + (821, 326, 0.1), + (821, 326-10, 0.1), + (821, 326+10, 0.1), + (821, 326, 0), + ]) + Equals to: + al.device.touch.down(403, 421) + time.sleep(0.2) + al.device.touch.move(821, 326) + time.sleep(0.1) + al.device.touch.move(821, 326-10) + time.sleep(0.1) + al.device.touch.move(821, 326+10) + time.sleep(0.1) + al.device.touch.up(821, 326) + """ + length = len(path) + for index, data in enumerate(path): + x, y, second = data + if index == 0: + self.device.touch.down(x, y) + logger.info(self._point2str(x, y) + ' down') + elif index - length == -1: + self.device.touch.up(x, y) + logger.info(self._point2str(x, y) + ' up') + else: + self.device.touch.move(x, y) + logger.info(self._point2str(x, y) + ' move') + self.sleep(second) + + def drag_node(self, location, offset=(-10, -10, 10, 10), sleep=0.25, reverse=False): + """Generate a swipe node. + + Args: + location (tuple): Origin location (x, y). + offset (tuple): set + sleep: + reverse (bool): Reverse offset. Default to false. + + Returns: + tuple: (x, y, sleep) + """ + if not reverse: + location = np.array(location) - random_rectangle_point(offset) + else: + location = np.array(location) + random_rectangle_point(offset) + location = tuple(np.append(location, [sleep])) + return location diff --git a/module/device/device.py b/module/device/device.py new file mode 100644 index 000000000..42167bf0a --- /dev/null +++ b/module/device/device.py @@ -0,0 +1,6 @@ +from module.device.control import Control +from module.device.screenshot import Screenshot + + +class Device(Screenshot, Control): + pass diff --git a/module/device/screenshot.py b/module/device/screenshot.py new file mode 100644 index 000000000..460677ce9 --- /dev/null +++ b/module/device/screenshot.py @@ -0,0 +1,90 @@ +import os +import time +from io import BytesIO + +from PIL import Image +from retrying import retry + +from module.device.connection import Connection + + +class Screenshot(Connection): + _screenshot_method = 0 + _screenshot_method_fixed = False + _adb = False + _last_save_time = {} + image: Image.Image + + def _screenshot_uiautomator2(self): + image = self.device.screenshot() + return image.convert('RGB') + + def _load_screenshot(self, screenshot): + if self._screenshot_method == 0: + return Image.open(BytesIO(screenshot)).convert('RGB') + elif self._screenshot_method == 1: + return Image.open(BytesIO(screenshot.replace(b'\r\n', b'\n'))).convert('RGB') + elif self._screenshot_method == 2: + return Image.open(BytesIO(screenshot.replace(b'\r\r\n', b'\n'))).convert('RGB') + + def _process_screenshot(self, screenshot): + if self._screenshot_method_fixed: + return self._load_screenshot(screenshot) + else: + for _ in range(3): + try: + screenshot = self._load_screenshot(screenshot) + except OSError: + self._screenshot_method += 1 + else: + self._screenshot_method_fixed = True + break + return screenshot + + def _screenshot_adb(self): + screenshot = self.adb_shell(['screencap', '-p'], serial=self.serial) + return self._process_screenshot(screenshot) + + @retry() + # @timer + def screenshot(self): + """ + Returns: + PIL.Image.Image: + """ + adb = self.config.USE_ADB_SREENSHOT + self._adb = adb + + if adb: + self.image = self._screenshot_adb() + else: + self.image = self._screenshot_uiautomator2() + + self.image.load() + return self.image + + def save_screenshot(self, genre='items'): + """Save a screenshot. Use millisecond timestamp as file name. + + Args: + genre (str, optional): Screenshot type. + + Returns: + bool: True if save succeed. + """ + now = time.time() + if now - self._last_save_time.get(genre, 0) > self.config.SCREEN_SHOT_SAVE_INTERVAL: + fmt = 'png' if self._adb else 'png' + file = '%s.%s' % (int(now * 1000), fmt) + + folder = os.path.join(self.config.SCREEN_SHOT_SAVE_FOLDER, genre) + if not os.path.exists(folder): + os.mkdir(folder) + + file = os.path.join(folder, file) + self.image.save(file) + self._last_save_time[genre] = now + return True + else: + self._last_save_time[genre] = now + return False diff --git a/module/equipment/assets.py b/module/equipment/assets.py new file mode 100644 index 000000000..e20fbd025 --- /dev/null +++ b/module/equipment/assets.py @@ -0,0 +1,17 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +EQUIPMENT_OPEN = Button(area=(1196, 114, 1263, 181), color=(119, 161, 213), button=(1196, 114, 1263, 181), file='./assets/equipment/EQUIPMENT_OPEN.png') +EQUIP_1 = Button(area=(1218, 249, 1274, 302), color=(102, 152, 219), button=(1218, 249, 1274, 302), file='./assets/equipment/EQUIP_1.png') +EQUIP_2 = Button(area=(1218, 349, 1274, 402), color=(101, 150, 215), button=(1218, 349, 1274, 402), file='./assets/equipment/EQUIP_2.png') +EQUIP_3 = Button(area=(1218, 450, 1274, 503), color=(98, 145, 211), button=(1218, 450, 1274, 503), file='./assets/equipment/EQUIP_3.png') +EQUIP_OFF = Button(area=(933, 548, 1063, 593), color=(107, 151, 216), button=(933, 548, 1063, 593), file='./assets/equipment/EQUIP_OFF.png') +EQUIP_OFF_CONFIRM = Button(area=(703, 492, 877, 550), color=(96, 144, 204), button=(703, 492, 877, 550), file='./assets/equipment/EQUIP_OFF_CONFIRM.png') +FLEET_ENTER = Button(area=(502, 474, 517, 489), color=(58, 62, 77), button=(502, 474, 517, 489), file='./assets/equipment/FLEET_ENTER.png') +FLEET_NEXT = Button(area=(1234, 327, 1254, 356), color=(72, 93, 125), button=(1234, 327, 1254, 356), file='./assets/equipment/FLEET_NEXT.png') +FLEET_PREV = Button(area=(26, 327, 46, 356), color=(72, 93, 125), button=(26, 327, 46, 356), file='./assets/equipment/FLEET_PREV.png') +OCR_FLEET_INDEX = Button(area=(958, 124, 984, 155), color=(46, 72, 117), button=(958, 124, 984, 155), file='./assets/equipment/OCR_FLEET_INDEX.png') +SWIPE_AREA = Button(area=(220, 167, 580, 527), color=(165, 172, 194), button=(220, 167, 580, 527), file='./assets/equipment/SWIPE_AREA.png') diff --git a/module/equipment/equipment.py b/module/equipment/equipment.py new file mode 100644 index 000000000..79f817934 --- /dev/null +++ b/module/equipment/equipment.py @@ -0,0 +1,149 @@ +from module.base.timer import Timer +from module.equipment.assets import * +from module.handler.info_bar import InfoBarHandler +from module.logger import logger +from module.ui.assets import BACK_ARROW + +SWIPE_DISTANCE = (350, 0) +SWIPE_RANDOM_RANGE = (-40, -20, 40, 20) + + +class Equipment(InfoBarHandler): + equipment_has_take_on = False + + def _view_next(self): + self.device.swipe(vector=(-SWIPE_DISTANCE[0], 0), box=SWIPE_AREA.area, random_range=SWIPE_RANDOM_RANGE, + padding=0, duration=(0.1, 0.12)) + self.wait_until_appear(EQUIPMENT_OPEN) + + def _view_prev(self): + self.device.swipe(vector=(SWIPE_DISTANCE[0], 0), box=SWIPE_AREA.area, random_range=SWIPE_RANDOM_RANGE, + padding=0, duration=(0.1, 0.12)) + self.wait_until_appear(EQUIPMENT_OPEN) + + def _equip_enter(self, enter): + enter_timer = Timer(5) + + while 1: + if enter_timer.reached(): + self.device.long_click(enter, duration=(1.5, 1.7)) + enter_timer.reset() + + self.device.screenshot() + + # End + if self.appear(EQUIPMENT_OPEN): + break + + def _equip_exit(self, out): + quit_timer = Timer(3) + + while 1: + self.device.screenshot() + + # End + if self.appear(out): + break + + if quit_timer.reached() and self.appear(BACK_ARROW): + # self.device.sleep(1) + self.device.click(BACK_ARROW) + self.device.sleep((0.2, 0.3)) + quit_timer.reset() + continue + + def _equip_take_off_one(self): + bar_timer = Timer(5) + off_timer = Timer(5) + confirm_timer = Timer(5) + + while 1: + self.device.screenshot() + if bar_timer.reached() and not self.appear(EQUIP_1, offset=10): + self.device.click(EQUIPMENT_OPEN) + bar_timer.reset() + continue + + if off_timer.reached() and not self.info_bar_count() and self.appear_then_click(EQUIP_OFF, offset=10): + off_timer.reset() + continue + + if confirm_timer.reached() and self.appear_then_click(EQUIP_OFF_CONFIRM, offset=10): + confirm_timer.reset() + continue + + # End + # if self.handle_info_bar(): + # break + if off_timer.started() and self.info_bar_count(): + break + + def equipment_take_off(self, enter, out, fleet): + """ + Args: + enter (Button): Long click to edit equipment. + out (Button): Button to confirm exit success. + fleet (list[int]): list of equipment record. [3, 1, 1, 1, 1, 1] + """ + logger.hr('Equipment take off') + self._equip_enter(enter) + + for index in '9'.join([str(x) for x in fleet if x > 0]): + index = int(index) + if index == 9: + self._view_next() + else: + self._equip_take_off_one() + + self._equip_exit(out) + self.equipment_has_take_on = False + + def _equip_take_on_one(self, index): + bar_timer = Timer(5) + on_timer = Timer(5) + + while 1: + self.device.screenshot() + + if bar_timer.reached() and not self.appear(EQUIP_1, offset=10): + self.device.click(EQUIPMENT_OPEN) + # self.device.sleep(0.3) + bar_timer.reset() + continue + + if on_timer.reached() and self.appear(EQUIP_1, offset=10) and not self.info_bar_count(): + if index == 1: + self.device.click(EQUIP_1) + elif index == 2: + self.device.click(EQUIP_2) + elif index == 3: + self.device.click(EQUIP_3) + + on_timer.reset() + continue + + # End + # if self.handle_info_bar(): + # break + if on_timer.started() and self.info_bar_count(): + break + + def equipment_take_on(self, enter, out, fleet): + """ + Args: + enter (Button): Long click to edit equipment. + out (Button): Button to confirm exit success. + fleet (list[int]): list of equipment record. [3, 1, 1, 1, 1, 1] + """ + logger.hr('Equipment take on') + self._equip_enter(enter) + + for index in '9'.join([str(x) for x in fleet if x > 0]): + index = int(index) + if index == 9: + self._view_next() + else: + self._equip_take_on_one(index=index) + + self._equip_exit(out) + self.equipment_has_take_on = True diff --git a/module/equipment/fleet_equipment.py b/module/equipment/fleet_equipment.py new file mode 100644 index 000000000..17069dcb6 --- /dev/null +++ b/module/equipment/fleet_equipment.py @@ -0,0 +1,37 @@ +from module.base.ocr import Digit +from module.equipment.assets import * +from module.equipment.equipment import Equipment +from module.ui.assets import FLEET_CHECK +from module.ui.ui import UI, page_fleet + +OCR_FLEET_INDEX = Digit(OCR_FLEET_INDEX, letter=(90, 154, 255), back=(24, 32, 49), length=1, white_list='123456') + + +class DailyEquipment(UI, Equipment): + equipment_has_take_on = False + + def equipment_take_on(self): + if self.config.FLEET_DAILY_EQUIPMENT is None: + return False + if self.equipment_has_take_on: + return False + + self.ui_ensure(page_fleet) + self.ui_ensure_index(self.config.FLEET_DAILY, letter=OCR_FLEET_INDEX, next_button=FLEET_NEXT, prev_button=FLEET_PREV) + super().equipment_take_on(enter=FLEET_ENTER, out=FLEET_CHECK, fleet=self.config.FLEET_DAILY_EQUIPMENT) + self.equipment_has_take_on = True + self.device.sleep(1) + return True + + def equipment_take_off(self): + if self.config.FLEET_DAILY_EQUIPMENT is None: + return False + if not self.equipment_has_take_on: + return False + + self.ui_ensure(page_fleet) + self.ui_ensure_index(self.config.FLEET_DAILY, letter=OCR_FLEET_INDEX, next_button=FLEET_NEXT, prev_button=FLEET_PREV) + super().equipment_take_off(enter=FLEET_ENTER, out=FLEET_CHECK, fleet=self.config.FLEET_DAILY_EQUIPMENT) + self.equipment_has_take_on = False + self.device.sleep(1) + return True diff --git a/module/event/campaign_ab.py b/module/event/campaign_ab.py new file mode 100644 index 000000000..607d15f54 --- /dev/null +++ b/module/event/campaign_ab.py @@ -0,0 +1,18 @@ +from module.campaign.run import CampaignRun + + +RECORD_SINCE = (0,) +CAMPAIGN_NAME = ['a1', 'a2', 'a3', 'b1', 'b2', 'b3'] + + +class CampaignAB(CampaignRun): + def run(self, name, folder='campaign_main'): + name = name.lower() + option = ('EventABRecord', name) + if not self.config.record_executed_since(option=option, since=RECORD_SINCE): + super().run(name=name, folder=folder) + self.config.record_save(option=option) + + def run_event_daily(self): + for name in CAMPAIGN_NAME: + self.run(name=name, folder=self.config.EVENT_NAME_AB) diff --git a/module/exercise/assets.py b/module/exercise/assets.py new file mode 100644 index 000000000..7d859616e --- /dev/null +++ b/module/exercise/assets.py @@ -0,0 +1,18 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +EQUIP_EDIT_ACTIVE = Button(area=(51, 608, 245, 668), color=(191, 159, 109), button=(51, 608, 245, 668), file='./assets/exercise/EQUIP_EDIT_ACTIVE.png') +EQUIP_EDIT_INACTIVE = Button(area=(51, 608, 246, 667), color=(89, 112, 158), button=(51, 608, 246, 667), file='./assets/exercise/EQUIP_EDIT_INACTIVE.png') +EQUIP_ENTER = Button(area=(351, 446, 361, 456), color=(252, 251, 252), button=(351, 446, 361, 456), file='./assets/exercise/EQUIP_ENTER.png') +EXERCISE_PREPARATION = Button(area=(543, 539, 741, 599), color=(235, 186, 114), button=(543, 539, 741, 599), file='./assets/exercise/EXERCISE_PREPARATION.png') +NEW_OPPONENT = Button(area=(1065, 340, 1204, 382), color=(129, 166, 220), button=(1065, 340, 1204, 382), file='./assets/exercise/NEW_OPPONENT.png') +OCR_EXERCISE_REMAIN = Button(area=(1148, 145, 1178, 160), color=(105, 148, 107), button=(1148, 145, 1178, 160), file='./assets/exercise/OCR_EXERCISE_REMAIN.png') +OPPONENT_1 = Button(area=(104, 77, 316, 381), color=(101, 117, 136), button=(104, 77, 316, 381), file='./assets/exercise/OPPONENT_1.png') +OPPONENT_2 = Button(area=(348, 77, 560, 381), color=(105, 123, 149), button=(348, 77, 560, 381), file='./assets/exercise/OPPONENT_2.png') +OPPONENT_3 = Button(area=(592, 77, 804, 381), color=(106, 131, 158), button=(592, 77, 804, 381), file='./assets/exercise/OPPONENT_3.png') +OPPONENT_4 = Button(area=(836, 77, 1048, 381), color=(103, 118, 141), button=(836, 77, 1048, 381), file='./assets/exercise/OPPONENT_4.png') +QUIT_CONFIRM = Button(area=(420, 490, 593, 548), color=(199, 122, 114), button=(420, 490, 593, 548), file='./assets/exercise/QUIT_CONFIRM.png') +QUIT_RECONFIRM = Button(area=(703, 492, 877, 550), color=(195, 111, 102), button=(703, 492, 877, 550), file='./assets/exercise/QUIT_RECONFIRM.png') diff --git a/module/exercise/combat.py b/module/exercise/combat.py new file mode 100644 index 000000000..e65a1bd1a --- /dev/null +++ b/module/exercise/combat.py @@ -0,0 +1,163 @@ +from module.combat.combat import * +from module.exercise.assets import * +from module.exercise.equipment import ExerciseEquipment +from module.exercise.hp_daemon import HpDaemon +from module.exercise.opponent import OpponentChoose, OPPONENT +from module.ui.assets import BACK_ARROW + + +class ExerciseCombat(HpDaemon, OpponentChoose, ExerciseEquipment): + def _in_exercise(self): + return self.appear(NEW_OPPONENT) + + def _combat_preparation(self): + logger.info('Combat preparation') + while 1: + self.device.screenshot() + if self.appear(BATTLE_PREPARATION): + + self.equipment_take_on() + pass + + self.device.click(BATTLE_PREPARATION) + continue + + # End + if self.appear(PAUSE): + break + + def _combat_execute(self): + """ + Returns: + bool: True if wins. False if quit. + """ + logger.info('Combat execute') + self.low_hp_confirm_timer = Timer(self.config.LOW_HP_CONFIRM_WAIT) + self.low_hp_confirm_timer.start() + show_hp_timer = Timer(5) + success = True + end = False + + while 1: + self.device.screenshot() + + # Finish + if self.appear_then_click(BATTLE_STATUS_S): + success = True + end = True + continue + if self.appear_then_click(GET_ITEMS_1): + continue + if self.appear_then_click(EXP_INFO_S): + continue + + # Quit + if not end: + if self._at_low_hp(image=self.device.image): + logger.info('Exercise quit') + if self.appear_then_click(PAUSE): + self.device.sleep(0.3) + continue + else: + if show_hp_timer.reached(): + show_hp_timer.reset() + self._show_hp() + + if self.appear_then_click(QUIT_CONFIRM, offset=True, interval=1): + success = False + end = True + continue + + if self.appear_then_click(QUIT_RECONFIRM, offset=True, interval=1): + self.interval_reset(QUIT_CONFIRM) + continue + + # End + if end and self.appear(BACK_ARROW): + logger.hr('Combat end') + break + + return success + + def _choose_opponent(self, index): + """ + Args: + index (int): From left to right. 0 to 3. + """ + logger.hr('Opponent: %s' % str(index)) + opponent_timer = Timer(1) + preparation_timer = Timer(1) + + while 1: + self.device.screenshot() + + if opponent_timer.reached() and self._in_exercise(): + self.device.click(OPPONENT[index, 0]) + opponent_timer.reset() + + if preparation_timer.reached() and self.appear_then_click(EXERCISE_PREPARATION): + # self.device.sleep(0.3) + preparation_timer.reset() + opponent_timer.reset() + continue + + # End + if self.appear(BATTLE_PREPARATION): + break + + def _preparation_quit(self): + logger.info('Preparation quit') + quit_timer = Timer(1) + + while 1: + self.device.screenshot() + + # End + if self._in_exercise(): + break + + if quit_timer.reached() and self.appear(BACK_ARROW): + # self.device.sleep(1) + self.device.click(BACK_ARROW) + quit_timer.reset() + continue + + def _combat(self, opponent): + """ + Args: + opponent(int): From left to right. 0 to 3. + + Returns: + bool: True if wins. False if challenge times exhausted. + """ + self._choose_opponent(opponent) + + for n in range(1, self.config.OPPONENT_CHALLENGE_TRIAL + 1): + logger.hr('Try: %s' % n) + self._combat_preparation() + success = self._combat_execute() + if success: + return success + + self._preparation_quit() + return False + + def equipment_take_off_when_finished(self): + if self.config.EXERCISE_FLEET_EQUIPMENT is None: + return False + if not self.equipment_has_take_on: + return False + + self._choose_opponent(0) + super().equipment_take_off() + self._preparation_quit() + + # def equipment_take_on(self): + # if self.config.EXERCISE_FLEET_EQUIPMENT is None: + # return False + # if self.equipment_has_take_on: + # return False + # + # self._choose_opponent(0) + # super().equipment_take_on() + # self._preparation_quit() diff --git a/module/exercise/equipment.py b/module/exercise/equipment.py new file mode 100644 index 000000000..9d60fc012 --- /dev/null +++ b/module/exercise/equipment.py @@ -0,0 +1,54 @@ +from module.base.timer import Timer +from module.combat.assets import BATTLE_PREPARATION +from module.equipment.equipment import Equipment +from module.exercise.assets import * + + +class ExerciseEquipment(Equipment): + def _active_edit(self): + timer = Timer(1) + while 1: + self.device.screenshot() + + if timer.reached() and self.appear_then_click(EQUIP_EDIT_INACTIVE): + timer.reset() + + # End + if self.appear(EQUIP_EDIT_ACTIVE): + self.device.sleep((0.2, 0.3)) + break + + def _inactive_edit(self): + timer = Timer(1) + while 1: + self.device.screenshot() + + if timer.reached() and self.appear_then_click(EQUIP_EDIT_ACTIVE): + timer.reset() + + # End + if self.appear(EQUIP_EDIT_INACTIVE): + self.device.sleep((0.2, 0.3)) + break + + def equipment_take_on(self): + if self.config.EXERCISE_FLEET_EQUIPMENT is None: + return False + if self.equipment_has_take_on: + return False + + self._active_edit() + super().equipment_take_on(enter=EQUIP_ENTER, out=BATTLE_PREPARATION, fleet=self.config.EXERCISE_FLEET_EQUIPMENT) + self._inactive_edit() + return True + + def equipment_take_off(self): + if self.config.EXERCISE_FLEET_EQUIPMENT is None: + return False + if not self.equipment_has_take_on: + return False + + self._active_edit() + super().equipment_take_off(enter=EQUIP_ENTER, out=BATTLE_PREPARATION, fleet=self.config.EXERCISE_FLEET_EQUIPMENT) + self._inactive_edit() + return True diff --git a/module/exercise/exercise.py b/module/exercise/exercise.py new file mode 100644 index 000000000..cf3a1c92d --- /dev/null +++ b/module/exercise/exercise.py @@ -0,0 +1,55 @@ +from module.base.ocr import Digit +from module.exercise.assets import * +from module.exercise.combat import ExerciseCombat +from module.logger import logger +from module.ui.ui import page_exercise + +OCR_EXERCISE_REMAIN = Digit(OCR_EXERCISE_REMAIN, letter=(173, 247, 74), back=(66, 89, 140), limit=10) +RECORD_OPTION = ('DailyRecord', 'exercise') +RECORD_SINCE = (0, 12, 18,) + + +class Exercise(ExerciseCombat): + opponent_change_count = 0 + + def _new_opponent(self): + logger.info('New opponent') + self.appear_then_click(NEW_OPPONENT) + self.opponent_change_count += 1 + self.device.sleep(1) + + def _exercise_once(self): + self._opponent_fleet_check_all() + while self.opponent_change_count <= 5: + for opponent in self._opponent_sort(): + success = self._combat(opponent) + if success: + return success + self._new_opponent() + return False + + def run(self): + self.ui_ensure(page_exercise) + # self.equipment_take_on() + # self.device.sleep(1) + + logger.hr('Exercise', level=1) + while 1: + self.device.screenshot() + remain = OCR_EXERCISE_REMAIN.ocr(self.device.image) + if remain == 0: + break + + logger.hr('Remain: %s' % remain) + success = self._exercise_once() + if not success: + logger.info('New opponent exhausted') + break + + self.equipment_take_off_when_finished() + + def record_executed_since(self): + return self.config.record_executed_since(option=RECORD_OPTION, since=RECORD_SINCE) + + def record_save(self): + return self.config.record_save(option=RECORD_OPTION) diff --git a/module/exercise/hp_daemon.py b/module/exercise/hp_daemon.py new file mode 100644 index 000000000..b1cfb034e --- /dev/null +++ b/module/exercise/hp_daemon.py @@ -0,0 +1,71 @@ +from module.base.base import ModuleBase +from module.base.timer import Timer +from module.base.utils import color_bar_percentage +from module.logger import logger + + +ATTACKER_HP_AREA = (271, 43, 586, 58) +DEFENDER_HP_AREA = (691, 43, 1005, 58) + + +class HpDaemon(ModuleBase): + attacker_hp = 1.0 + defender_hp = 1.0 + # _last_secure_time = 0 + low_hp_confirm_timer: Timer + + @staticmethod + def _calculate_hp(image, area, reverse=False, starter=2, prev_color=(239, 32, 33)): + """ + Args: + image: + area: + reverse: True if HP is left align. + starter: + prev_color: + + Returns: + float: HP. 0 to 1. + """ + # bar = np.array(image.crop(area)) + # length = bar.shape[1] + # bar = np.swapaxes(bar, 0, 1) + # bar = bar[::-1, :, :] if reverse else bar + # prev_index = 0 + # for index, color in enumerate(bar): + # if index < starter: + # continue + # mask = color_similar_1d(color, prev_color, threshold=30) + # if np.any(mask): + # prev_color = color[mask].mean(axis=0) + # prev_index = index + # + # return prev_index / length + return color_bar_percentage(image, area, prev_color=prev_color, starter=starter, reverse=reverse) + + def _show_hp(self, low_hp_time=0): + """ + Examples: + [ 80% - 70%] + [ 80% - 70%] + [ 80% - 70%] - Low HP: 3.154s + """ + text = '[%s - %s]' % ( + str(int(self.attacker_hp * 100)).rjust(2, '0') + '%', + str(int(self.defender_hp * 100)).rjust(2, '0') + '%') + if low_hp_time: + text += ' - Low HP: %ss' % str(round(low_hp_time, 3)).ljust(5, '0') + logger.info(text) + + def _at_low_hp(self, image): + self.attacker_hp = self._calculate_hp(image, area=ATTACKER_HP_AREA, reverse=True) + self.defender_hp = self._calculate_hp(image, area=DEFENDER_HP_AREA, reverse=False) + if 0.01 < self.attacker_hp <= self.config.LOW_HP_THRESHOLD: + if self.low_hp_confirm_timer.reached() and self.low_hp_confirm_timer.current() < 300: + self._show_hp(self.low_hp_confirm_timer.current()) + return True + else: + return False + else: + self.low_hp_confirm_timer.reset() + return False diff --git a/module/exercise/opponent.py b/module/exercise/opponent.py new file mode 100644 index 000000000..2152b306b --- /dev/null +++ b/module/exercise/opponent.py @@ -0,0 +1,86 @@ +import numpy as np + +from module.base.button import ButtonGrid +from module.base.ocr import Digit +from module.exercise.assets import * +from module.logger import logger +from module.ui.assets import BACK_ARROW +from module.ui.ui import UI + +OPPONENT = ButtonGrid(origin=(104, 77), delta=(244, 0), button_shape=(212, 304), grid_shape=(4, 1)) + + +class Opponent: + def __init__(self, main_image, fleet_image, index): + self.index = index + self.power = self.get_power(image=main_image) + self.level = self.get_level(image=fleet_image) + self.priority = self.get_priority() + + # [OPPONENT_1] ( 8256) 120 120 120 | (12356) 100 80 80 + level = [str(x).rjust(3, ' ') for x in self.level] + power = ['(' + str(x).rjust(5, ' ') + ')' for x in self.power] + logger.attr( + 'OPPONENT_%s, %s' % (index, str(np.round(self.priority, 3)).ljust(5, '0')), + ' '.join([power[0]] + level[:3] + ['|'] + [power[1]] + level[3:]) + ) + + @staticmethod + def process(image): + # image[-6:, :] = 255 + letter_l = np.where(np.mean(image, axis=0) < 85)[0] + if len(letter_l): + letter_l = letter_l[0] + 75 + image = image[:, letter_l:] + + image = np.pad(image, ((0, 0), (0, 5)), mode='constant', constant_values=255) + + return image + + def get_level(self, image): + level = [] + level += ButtonGrid(origin=(130, 259), delta=(168, 0), button_shape=(57, 21), grid_shape=(3, 1), name='LEVEL').buttons() + level += ButtonGrid(origin=(832, 259), delta=(168, 0), button_shape=(57, 21), grid_shape=(3, 1), name='LEVEL').buttons() + + level = Digit(level, letter=(255, 255, 255), back=(102, 102, 102), limit=120, threshold=127, additional_preprocess=self.process, name='LEVEL') + result = level.ocr(image) + return result + + def get_power(self, image): + grids = ButtonGrid(origin=(222, 266), delta=(244, 30), button_shape=(72, 15), grid_shape=(4, 2), name='POWER') + power = [grids[self.index, 0], grids[self.index, 1]] + + power = Digit(power, letter=(255, 223, 57), back=(74, 109, 156), threshold=221, limit=17000, name='POWER') + result = power.ocr(image) + return result + + def get_priority(self): + # level = np.sum(self.level) / 6 + # power = np.sum(self.power) / 6 + # return level - (power - 1000) / 30 + + level = np.sum(self.level) / 6 + return level + + +class OpponentChoose(UI): + main_image = None + opponents = [] + + def _opponent_fleet_check_all(self): + self.opponents = [] + self.main_image = self.device.image + + for index in range(4): + self.ui_click(click_button=OPPONENT[index, 0], check_button=EXERCISE_PREPARATION, + appear_button=NEW_OPPONENT, skip_first_screenshot=True) + + self.opponents.append(Opponent(main_image=self.main_image, fleet_image=self.device.image, index=index)) + + self.ui_click(click_button=BACK_ARROW, check_button=NEW_OPPONENT, + appear_button=EXERCISE_PREPARATION, skip_first_screenshot=True) + + def _opponent_sort(self): + priority = np.argsort([x.priority for x in self.opponents])[::-1] + logger.attr('Order', str(priority)) + return priority diff --git a/module/handler/ambush.py b/module/handler/ambush.py new file mode 100644 index 000000000..2760136b6 --- /dev/null +++ b/module/handler/ambush.py @@ -0,0 +1,89 @@ +import numpy as np + +from module.base.timer import Timer +from module.base.utils import red_overlay_transparency, get_color +from module.combat.combat import Combat +from module.handler.assets import * +from module.logger import logger + + +def ambush_letter_preprocess(image): + """ + Args: + image (np.ndarray): + + Returns: + np.ndarray + """ + return (image - 64) / 0.75 + + +TEMPLATE_AMBUSH_EVADE_SUCCESS.set_preprocess_func(ambush_letter_preprocess) +TEMPLATE_AMBUSH_EVADE_FAILED.set_preprocess_func(ambush_letter_preprocess) + + +class AmbushHandler(Combat): + MAP_AMBUSH_OVERLAY_TRANSPARENCY_THRESHOLD = 0.40 + MAP_AIR_RAID_OVERLAY_TRANSPARENCY_THRESHOLD = 0.35 # Usually (0.50, 0.53) + MAP_AIR_RAID_CONFIRM_SECOND = 0.5 + + def ambush_color_initial(self): + MAP_AMBUSH.load_color(self.device.image) + MAP_AIR_RAID.load_color(self.device.image) + + def _ambush_appear(self): + return red_overlay_transparency(MAP_AMBUSH.color, get_color(self.device.image, MAP_AMBUSH.area)) > \ + self.MAP_AMBUSH_OVERLAY_TRANSPARENCY_THRESHOLD + + def _air_raid_appear(self): + return red_overlay_transparency(MAP_AIR_RAID.color, get_color(self.device.image, MAP_AIR_RAID.area)) > \ + self.MAP_AIR_RAID_OVERLAY_TRANSPARENCY_THRESHOLD + + def _handle_air_raid(self): + logger.info('Map air raid') + disappear = Timer(self.MAP_AIR_RAID_CONFIRM_SECOND) + disappear.start() + while 1: + self.device.screenshot() + if self._air_raid_appear(): + disappear.reset() + else: + if disappear.reached(): + break + + def _handle_ambush(self): + logger.info('Map ambushed') + self.wait_until_appear_then_click(MAP_AMBUSH_EVADE) + # self.sleep(0.8) + # while 1: + # self.screenshot() + # if self.handle_info_bar(): + # if self.combat_appear(): + # logger.info('Ambush evade failed') + # self.combat() + # break + self.wait_until_appear(INFO_BAR_1) + if TEMPLATE_AMBUSH_EVADE_SUCCESS.match(self.device.image): + logger.info('Ambush evade success') + elif TEMPLATE_AMBUSH_EVADE_FAILED.match(self.device.image): + logger.info('Ambush evade failed') + self.combat(expected_end='no_searching') + else: + logger.warning('Unrecognised info when ambush evade.') + self.ensure_no_info_bar() + if self.combat_appear(): + self.combat() + + def handle_ambush(self): + if not self.config.MAP_HAS_AMBUSH: + return False + + if self._air_raid_appear(): + self._handle_air_raid() + return True + + if self._ambush_appear(): + self._handle_ambush() + return True + + return False diff --git a/module/handler/assets.py b/module/handler/assets.py new file mode 100644 index 000000000..58a3a0e2b --- /dev/null +++ b/module/handler/assets.py @@ -0,0 +1,23 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +GET_AMMO = Button(area=(792, 315, 794, 333), color=(251, 250, 255), button=(792, 315, 794, 333), file='./assets/handler/GET_AMMO.png') +GET_ITEMS_1 = Button(area=(538, 217, 741, 253), color=(160, 192, 248), button=(924, 653, 943, 672), file='./assets/handler/GET_ITEMS_1.png') +GET_MISSION = Button(area=(553, 491, 727, 551), color=(96, 144, 204), button=(553, 491, 727, 551), file='./assets/handler/GET_MISSION.png') +INFO_BAR_1 = Button(area=(194, 297, 1086, 299), color=(107, 158, 255), button=(194, 297, 1086, 299), file='./assets/handler/INFO_BAR_1.png') +INFO_BAR_2 = Button(area=(194, 234, 1086, 236), color=(107, 158, 255), button=(194, 234, 1086, 236), file='./assets/handler/INFO_BAR_2.png') +INFO_BAR_3 = Button(area=(194, 171, 1086, 173), color=(107, 158, 255), button=(194, 171, 1086, 173), file='./assets/handler/INFO_BAR_3.png') +IN_MAP = Button(area=(749, 654, 921, 707), color=(213, 124, 124), button=(749, 654, 921, 707), file='./assets/handler/IN_MAP.png') +IN_STAGE_BLUE = Button(area=(20, 641, 148, 702), color=(154, 177, 226), button=(20, 641, 148, 702), file='./assets/handler/IN_STAGE_BLUE.png') +IN_STAGE_RED = Button(area=(22, 643, 150, 704), color=(229, 159, 159), button=(22, 643, 150, 704), file='./assets/handler/IN_STAGE_RED.png') +MAP_AIR_RAID = Button(area=(350, 447, 1280, 472), color=(154, 43, 46), button=(350, 447, 1280, 472), file='./assets/handler/MAP_AIR_RAID.png') +MAP_AMBUSH = Button(area=(261, 433, 1280, 449), color=(161, 41, 43), button=(261, 433, 1280, 449), file='./assets/handler/MAP_AMBUSH.png') +MAP_AMBUSH_EVADE = Button(area=(325, 393, 1280, 395), color=(255, 255, 255), button=(979, 444, 1152, 502), file='./assets/handler/MAP_AMBUSH_EVADE.png') +MAP_ENEMY_SEARCHING = Button(area=(531, 320, 864, 382), color=(200, 99, 91), button=(531, 320, 864, 382), file='./assets/handler/MAP_ENEMY_SEARCHING.png') +MYSTERY_ITEM = Button(area=(589, 294, 691, 427), color=(144, 127, 83), button=(589, 294, 691, 427), file='./assets/handler/MYSTERY_ITEM.png') +STRATEGY_OPEN_1 = Button(area=(1176, 366, 1275, 393), color=(128, 155, 218), button=(1060, 406, 1092, 485), file='./assets/handler/STRATEGY_OPEN_1.png') +TEMPLATE_AMBUSH_EVADE_FAILED = Template(area=(641, 311, 692, 336), color=(107, 110, 118), button=(641, 311, 692, 336), file='./assets/handler/TEMPLATE_AMBUSH_EVADE_FAILED.png') +TEMPLATE_AMBUSH_EVADE_SUCCESS = Template(area=(641, 311, 692, 336), color=(101, 101, 112), button=(641, 311, 692, 336), file='./assets/handler/TEMPLATE_AMBUSH_EVADE_SUCCESS.png') diff --git a/module/handler/enemy_searching.py b/module/handler/enemy_searching.py new file mode 100644 index 000000000..e84c97355 --- /dev/null +++ b/module/handler/enemy_searching.py @@ -0,0 +1,66 @@ +from module.base.timer import Timer +from module.base.utils import red_overlay_transparency, get_color +from module.handler.assets import * +from module.handler.info_bar import InfoBarHandler +from module.logger import logger + + +class EnemySearchingHandler(InfoBarHandler): + MAP_ENEMY_SEARCHING_OVERLAY_TRANSPARENCY_THRESHOLD = 0.5 # Usually (0.70, 0.80). + MAP_ENEMY_SEARCHING_TIMEOUT_SECOND = 4 + + def _color_initial(self): + MAP_ENEMY_SEARCHING.load_color(self.device.image) + + def _enemy_searching_appear(self): + return red_overlay_transparency( + MAP_ENEMY_SEARCHING.color, get_color(self.device.image, MAP_ENEMY_SEARCHING.area) + ) > self.MAP_ENEMY_SEARCHING_OVERLAY_TRANSPARENCY_THRESHOLD + + def _handle_enemy_flashing(self): + self.device.sleep(1) + + def handle_in_stage(self): + if self.appear(IN_STAGE_RED) or self.appear(IN_STAGE_BLUE): + logger.info('In stage.') + # self.device.sleep(0.5) + self.ensure_no_info_bar(timeout=0.6) + return True + else: + return False + + def is_in_map(self): + return self.appear(IN_MAP) + + def handle_in_map_with_enemy_searching(self): + if not self.is_in_map(): + return False + + timeout = Timer(self.MAP_ENEMY_SEARCHING_TIMEOUT_SECOND) + appeared = False + while 1: + timeout.start() + if self._enemy_searching_appear(): + appeared = True + else: + if appeared: + self._handle_enemy_flashing() + self.device.sleep(0.3) + logger.info('In map.') + break + self._color_initial() + + if timeout.reached(): + # logger.warning('Enemy searching timeout.') + logger.info('Enemy searching timeout.') + break + + self.device.screenshot() + return True + + def handle_in_map(self): + if not self.is_in_map(): + return False + + self.device.sleep((1, 1.2)) + return True diff --git a/module/handler/info_bar.py b/module/handler/info_bar.py new file mode 100644 index 000000000..481501aa2 --- /dev/null +++ b/module/handler/info_bar.py @@ -0,0 +1,36 @@ +from module.base.base import ModuleBase +from module.base.timer import Timer +from module.handler.assets import * + + +class InfoBarHandler(ModuleBase): + def info_bar_count(self): + if self.appear(INFO_BAR_1): + # if self.appear(INFO_BAR_2): + # if self.appear(INFO_BAR_3): + # return 3 + # else: + # return 2 + # else: + # return 1 + return 1 + else: + return 0 + + def handle_info_bar(self): + if self.info_bar_count(): + self.wait_until_disappear(INFO_BAR_1) + return True + else: + return False + # self.sleep(1) + + def ensure_no_info_bar(self, timeout=0.6): + timeout = Timer(timeout) + timeout.start() + while 1: + self.device.screenshot() + self.handle_info_bar() + + if timeout.reached(): + break diff --git a/module/handler/mystery.py b/module/handler/mystery.py new file mode 100644 index 000000000..b2ac9a781 --- /dev/null +++ b/module/handler/mystery.py @@ -0,0 +1,44 @@ +from module.base.timer import Timer +from module.base.utils import area_in_area +from module.handler.assets import * +from module.handler.strategy import StrategyHandler +from module.logger import logger + + +class MysteryHandler(StrategyHandler): + _get_ammo_log_timer = Timer(3) + + def handle_mystery(self, button=None): + """ + Args: + button (optional): Button to click when get_items. + Can be destination grid which makes the bot more like human. + """ + if button is None or area_in_area(button.button, MYSTERY_ITEM.area, threshold=20): + button = GET_ITEMS_1 + + if self.appear(GET_ITEMS_1): + logger.attr('Mystery', 'Get item') + self._save_mystery_image() + self.device.click(button) + self.device.sleep(0.5) + self.device.screenshot() + self.handle_opened_strategy_bar() + return True + + if self.info_bar_count(): + if self._get_ammo_log_timer.reached() and self.appear(GET_AMMO): + logger.attr('Mystery', 'Get ammo') + self._get_ammo_log_timer.reset() + self._save_mystery_image() + + return True + + # if self.handle_info_bar(): + # return True + + return False + + def _save_mystery_image(self): + if self.config.ENABLE_SAVE_GET_ITEMS: + self.device.save_screenshot('mystery') diff --git a/module/handler/strategy.py b/module/handler/strategy.py new file mode 100644 index 000000000..2c13866d8 --- /dev/null +++ b/module/handler/strategy.py @@ -0,0 +1,11 @@ +from module.handler.assets import STRATEGY_OPEN_1 +from module.handler.info_bar import InfoBarHandler + + +class StrategyHandler(InfoBarHandler): + def handle_opened_strategy_bar(self): + if self.appear_then_click(STRATEGY_OPEN_1): + self.device.sleep(0.5) + return True + + return False diff --git a/module/handler/urgent_commission.py b/module/handler/urgent_commission.py new file mode 100644 index 000000000..987de2fe7 --- /dev/null +++ b/module/handler/urgent_commission.py @@ -0,0 +1,25 @@ +from module.handler.assets import * +from module.handler.info_bar import InfoBarHandler +from module.logger import logger + + +class UrgentCommissionHandler(InfoBarHandler): + def handle_urgent_commission(self, save_get_items=None): + """ + Args: + save_get_items (bool): + + Returns: + bool: + """ + if save_get_items is None: + save_get_items = self.config.ENABLE_SAVE_GET_ITEMS + + appear = self.appear_then_click(GET_MISSION, offset=True, interval=2) + if appear: + logger.info('Get urgent commission') + if save_get_items: + if self.handle_info_bar(): + self.device.screenshot() + self.device.save_screenshot('get_mission') + return appear diff --git a/module/hard/assets.py b/module/hard/assets.py new file mode 100644 index 000000000..2c2368eec --- /dev/null +++ b/module/hard/assets.py @@ -0,0 +1,9 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +EQUIP_ENTER_1 = Button(area=(712, 163, 782, 233), color=(59, 63, 66), button=(712, 163, 782, 233), file='./assets/hard/EQUIP_ENTER_1.png') +EQUIP_ENTER_2 = Button(area=(713, 296, 783, 366), color=(60, 63, 63), button=(713, 296, 783, 366), file='./assets/hard/EQUIP_ENTER_2.png') +OCR_HARD_REMAIN = Button(area=(98, 612, 109, 628), color=(79, 141, 42), button=(98, 612, 109, 628), file='./assets/hard/OCR_HARD_REMAIN.png') diff --git a/module/hard/equipment.py b/module/hard/equipment.py new file mode 100644 index 000000000..2506baa51 --- /dev/null +++ b/module/hard/equipment.py @@ -0,0 +1,25 @@ +from module.equipment.equipment import Equipment +from module.hard.assets import * +from module.map.assets import * + + +class HardEquipment(Equipment): + def equipment_take_on(self): + if self.config.FLEET_HARD_EQUIPMENT is None: + return False + if self.equipment_has_take_on: + return False + + enter = EQUIP_ENTER_1 if self.config.FLEET_HARD == 1 else EQUIP_ENTER_2 + super().equipment_take_on(enter=enter, out=FLEET_PREPARATION, fleet=self.config.FLEET_HARD_EQUIPMENT) + return True + + def equipment_take_off(self): + if self.config.FLEET_HARD_EQUIPMENT is None: + return False + if not self.equipment_has_take_on: + return False + + enter = EQUIP_ENTER_1 if self.config.FLEET_HARD == 1 else EQUIP_ENTER_2 + super().equipment_take_off(enter=enter, out=FLEET_PREPARATION, fleet=self.config.FLEET_HARD_EQUIPMENT) + return True diff --git a/module/hard/hard.py b/module/hard/hard.py new file mode 100644 index 000000000..de97551ff --- /dev/null +++ b/module/hard/hard.py @@ -0,0 +1,50 @@ +import importlib + +from module.base.ocr import Digit +from module.campaign.campaign_hard import Campaign +from module.campaign.run import CampaignRun +from module.hard.assets import * +from module.logger import logger + +OCR_HARD_REMAIN = Digit(OCR_HARD_REMAIN, letter=(123, 227, 66), back=(24, 24, 24), limit=3, name='OCR_HARD_REMAIN') +RECORD_OPTION = ('DailyRecord', 'hard') +RECORD_SINCE = (0,) + + +class CampaignHard(CampaignRun): + equipment_has_take_on = False + campaign: Campaign + + def run(self): + logger.hr('Campaign hard', level=1) + chapter, stage = self.config.HARD_CAMPAIGN.split('-') + name = f'campaign_{chapter}_{stage}' + + # Initial + self.load_campaign(name='campaign_hard', folder='campaign_hard') # Load campaign file + module = importlib.import_module('.' + name, 'campaign.campaign_main') # Load map from normal mode. + self.campaign.MAP = module.MAP + self.campaign_name_set(name + '_HARD') + if self.equipment_has_take_on: + self.campaign.equipment_has_take_on = True + + # UI ensure + self.ui_weigh_anchor() + self.campaign_ensure_mode(mode='normal') + self.campaign_ensure_chapter(index=int(chapter)) + self.campaign_ensure_mode(mode='hard') + self.campaign.ENTRANCE = self.campaign_get_entrance(name=f'{chapter}-{stage}') + + # Run + remain = OCR_HARD_REMAIN.ocr(self.device.image) + logger.attr('Remain', remain) + for n in range(remain): + self.campaign.run() + + self.campaign.equipment_take_off_when_finished() + + def record_executed_since(self): + return self.config.record_executed_since(option=RECORD_OPTION, since=RECORD_SINCE) + + def record_save(self): + return self.config.record_save(option=RECORD_OPTION) diff --git a/module/logger.py b/module/logger.py new file mode 100644 index 000000000..8c6f1bc4a --- /dev/null +++ b/module/logger.py @@ -0,0 +1,43 @@ +import logging +import datetime + + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +formatter = logging.Formatter(fmt='%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + +console = logging.StreamHandler() +console.setFormatter(formatter) +logger.addHandler(console) + +log_file = './log/%s.txt' % datetime.date.today() +file = logging.FileHandler(log_file) +file.setFormatter(formatter) +logger.addHandler(file) + + +def hr(title, level=3): + title = str(title).upper() + if level == 1: + logger.info('=' * 20 + ' ' + title + ' ' + '=' * 20) + if level == 2: + logger.info('-' * 20 + ' ' + title + ' ' + '-' * 20) + if level == 3: + logger.info('<' * 3 + ' ' + title + ' ' + '>' * 3) + if level == 0: + middle = '|' + ' ' * 20 + title + ' ' * 20 + '|' + border = '+' + '-' * (len(middle) - 2) + '+' + logger.info(border) + logger.info(middle) + logger.info(border) + + +def attr(name, text): + logger.info('[%s] %s' % (str(name), str(text))) + + +logger.hr = hr +logger.attr = attr + +logger.hr('Start', level=0) diff --git a/module/map/assets.py b/module/map/assets.py new file mode 100644 index 000000000..7ea54110e --- /dev/null +++ b/module/map/assets.py @@ -0,0 +1,29 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +FLEET_1_BAR = Button(area=(1015, 244, 1186, 490), color=(171, 169, 170), button=(1015, 244, 1186, 490), file='./assets/map/FLEET_1_BAR.png') +FLEET_1_CHOOSE = Button(area=(1026, 166, 1089, 224), color=(209, 166, 106), button=(1026, 166, 1089, 224), file='./assets/map/FLEET_1_CHOOSE.png') +FLEET_1_CLEAR = Button(area=(1108, 166, 1172, 224), color=(154, 154, 156), button=(1108, 166, 1172, 224), file='./assets/map/FLEET_1_CLEAR.png') +FLEET_1_IN_USE = Button(area=(390, 158, 473, 241), color=(197, 171, 159), button=(390, 158, 473, 241), file='./assets/map/FLEET_1_IN_USE.png') +FLEET_2_BAR = Button(area=(1015, 377, 1186, 623), color=(180, 171, 155), button=(1015, 377, 1186, 623), file='./assets/map/FLEET_2_BAR.png') +FLEET_2_CHOOSE = Button(area=(1026, 299, 1089, 357), color=(208, 165, 105), button=(1026, 299, 1089, 357), file='./assets/map/FLEET_2_CHOOSE.png') +FLEET_2_CLEAR = Button(area=(1108, 299, 1172, 357), color=(153, 154, 156), button=(1108, 299, 1172, 357), file='./assets/map/FLEET_2_CLEAR.png') +FLEET_2_IN_USE = Button(area=(390, 291, 473, 374), color=(74, 66, 66), button=(390, 291, 473, 374), file='./assets/map/FLEET_2_IN_USE.png') +FLEET_LOCKED = Button(area=(1180, 529, 1280, 530), color=(82, 162, 115), button=(1181, 491, 1279, 528), file='./assets/map/FLEET_LOCKED.png') +FLEET_PREPARATION = Button(area=(981, 575, 1180, 636), color=(235, 185, 114), button=(981, 575, 1180, 636), file='./assets/map/FLEET_PREPARATION.png') +FLEET_PREPARATION_HARD_1 = Button(area=(200, 260, 1004, 261), color=(247, 215, 0), button=(200, 260, 1004, 261), file='./assets/map/FLEET_PREPARATION_HARD_1.png') +FLEET_PREPARATION_HARD_2 = Button(area=(201, 393, 1001, 394), color=(255, 219, 0), button=(201, 393, 1001, 394), file='./assets/map/FLEET_PREPARATION_HARD_2.png') +FLEET_UNLOCKED = Button(area=(1180, 529, 1280, 530), color=(222, 130, 115), button=(1181, 491, 1279, 528), file='./assets/map/FLEET_UNLOCKED.png') +MAP_CAT_ATTACK = Button(area=(1241, 106, 1271, 115), color=(255, 231, 123), button=(1148, 653, 1262, 705), file='./assets/map/MAP_CAT_ATTACK.png') +MAP_OFFENSIVE = Button(area=(1148, 653, 1262, 705), color=(234, 180, 108), button=(1148, 653, 1262, 705), file='./assets/map/MAP_OFFENSIVE.png') +MAP_PREPARATION = Button(area=(854, 488, 1052, 548), color=(236, 186, 115), button=(854, 488, 1052, 548), file='./assets/map/MAP_PREPARATION.png') +SUBMARINE_BAR = Button(area=(1015, 525, 1186, 602), color=(193, 177, 144), button=(1015, 525, 1186, 602), file='./assets/map/SUBMARINE_BAR.png') +SUBMARINE_CHOOSE = Button(area=(1026, 447, 1090, 505), color=(208, 164, 103), button=(1026, 447, 1090, 505), file='./assets/map/SUBMARINE_CHOOSE.png') +SUBMARINE_CLEAR = Button(area=(1108, 447, 1172, 505), color=(152, 153, 154), button=(1108, 447, 1172, 505), file='./assets/map/SUBMARINE_CLEAR.png') +SUBMARINE_IN_USE = Button(area=(454, 439, 537, 521), color=(42, 54, 76), button=(454, 439, 537, 521), file='./assets/map/SUBMARINE_IN_USE.png') +SWITCH_OVER = Button(area=(947, 654, 1118, 706), color=(144, 158, 198), button=(947, 654, 1118, 706), file='./assets/map/SWITCH_OVER.png') +WITHDRAW = Button(area=(749, 654, 921, 707), color=(213, 124, 124), button=(749, 654, 921, 707), file='./assets/map/WITHDRAW.png') +WITHDRAW_CONFIRM = Button(area=(703, 492, 877, 551), color=(95, 143, 203), button=(703, 492, 877, 551), file='./assets/map/WITHDRAW_CONFIRM.png') diff --git a/module/map/camera.py b/module/map/camera.py new file mode 100644 index 000000000..73593ab92 --- /dev/null +++ b/module/map/camera.py @@ -0,0 +1,289 @@ +import numpy as np + +from module.handler.info_bar import InfoBarHandler +from module.logger import logger +from module.map.grids import Grids, Grid +from module.map.map_base import CampaignMap, location2node, location_ensure + +param_x = np.array([4.41071252e+00, -3.90656142e-03, 1.95849683e+02]) +param_y_positive = np.array([4.88226475e+00, -2.79093133e-03, 1.39520005e+02]) +param_y_negative = np.array([4.58377745e+00, 3.13441976e-04, 1.39028669e+02]) + + +def swipe_multiply_1d(x, a, b, c): + return a / (x + b) + c + + +def swipe_multiply_2d(x, y): + if abs(x) > 0.3: + x = swipe_multiply_1d(abs(x), *param_x) * x + else: + x = x * 200 + + if abs(y) > 0.3: + y = swipe_multiply_1d(y, *param_y_positive) * y if y > 0 else swipe_multiply_1d(-y, *param_y_negative) * y + else: + y = y * 140 + + return x, y + + +class Camera(InfoBarHandler): + map: CampaignMap + camera = (0, 0) + + def _map_swipe(self, vector, drop_threshold=0.1): + """ + Args: + vector(tuple, np.ndarray): float + drop_threshold(float): swipe distance lower than this will be drop, because a closing swipe will be treat + as a click in game. + Returns: + bool: if camera moved. + """ + x, y = vector + if abs(x) > drop_threshold or abs(y) > drop_threshold: + # Linear fit + # x = x * 200 + # y = y * 140 + # Function fit + x, y = swipe_multiply_2d(x, y) + + vector = (-x, -y) + self.device.swipe(vector) + self.device.sleep(0.3) + self.update() + else: + self.update(camera=False) + + def map_swipe(self, vector): + """ + Swipe to a grid using relative position. + Remember to update before calling this. + + Args: + vector(tuple): int + Returns: + bool: if camera moved. + """ + logger.info('Map swipe: %s' % str(vector)) + vector = np.array(vector) + self.camera = tuple(vector + self.camera) + vector = np.array([0.5, 0.5]) - np.array(self.grids.center_offset) + vector + self._map_swipe(vector) + + def update(self, camera=True): + """Update map image + + Args: + camera: True to update camera position and perspective data. + """ + self.device.screenshot() + if not camera: + self.grids.update(image=self.device.image) + return True + + self.grids = Grids(self.device.image, config=self.config) + + # Catch perspective error + known_exception = self.info_bar_count() + if len(self.grids.horizontal) > self.map.shape[1] + 2 or len(self.grids.vertical) > self.map.shape[0] + 2: + if not known_exception: + logger.warn('Perspective Error. Too many lines') + self.grids.correct = False + if len(self.grids.horizontal) <= 3 or len(self.grids.vertical) <= 3: + if not known_exception: + logger.warn('Perspective Error. Too few lines') + self.grids.correct = False + + if not self.grids.correct: + if self.info_bar_count(): + logger.info('Perspective error cause by info bar. Waiting.') + self.handle_info_bar() + return self.update(camera=camera) + else: + self.grids.save_error_image() + + # Set camera position + + if self.grids.left_edge: + x = 0 + self.grids.center_grid[0] + elif self.grids.right_edge: + x = self.map.shape[0] - self.grids.shape[0] + self.grids.center_grid[0] + else: + x = self.camera[0] + if self.grids.lower_edge: + y = 0 + self.grids.center_grid[1] + elif self.grids.upper_edge: + y = self.map.shape[1] - self.grids.shape[1] + self.grids.center_grid[1] + else: + y = self.camera[1] + self.camera = (x, y) + + # align_x = self.map.shape[0] - self.grids.shape[0] if self.grids.right_edge else 0 + # align_y = self.map.shape[1] - self.grids.shape[1] if self.grids.upper_edge else 0 + # self.camera = np.array((align_x, align_y)) + self.grids.center_grid + self.show_camera() + + def predict(self): + self.grids.predict() + self.map.update(grids=self.grids, camera=self.camera) + + def show_camera(self): + logger.info(' Camera: %s' % location2node(self.camera)) + + def ensure_edge_insight(self, reverse=False, preset=None): + """ + Swipe to bottom left until two edges insight. + Edges are used to locate camera. + + Args: + reverse (bool): Reverse swipes. + preset (tuple(int)): Set in map swipe manually. + + Returns: + list[tuple]: Swipe record. + """ + logger.info('Ensure edge in sight.') + record = [] + self.update() + + if preset is not None: + self.map_swipe(preset) + record.append(preset) + self.update() + + while 1: + x = 0 if self.grids.left_edge or self.grids.right_edge else 3 + y = 0 if self.grids.lower_edge or self.grids.upper_edge else 2 + + # Swipe even if two edges insight, this will avoid some embarrassing camera position. + self.map_swipe((x, y)) + record.append((x, y)) + + if x == 0 and y == 0: + break + + if reverse: + logger.info('Reverse swipes.') + for vector in record[::-1]: + x, y = vector + if x != 0 and y != 0: + self.map_swipe((-x, -y)) + + return record + + def focus_to(self, location, swipe_limit=(3, 2)): + """Focus camera on a grid + + Args: + location: grid + swipe_limit(tuple): (x, y). Limit swipe in (-x, -y, x, y). + """ + location = location_ensure(location) + logger.info('Focus to: %s' % location2node(location)) + + vector = np.array(location) - self.camera + vector, sign = np.abs(vector), np.sign(vector) + while 1: + + swipe = ( + vector[0] if vector[0] < swipe_limit[0] else swipe_limit[0], + vector[1] if vector[1] < swipe_limit[1] else swipe_limit[1] + ) + self.map_swipe(tuple(sign * swipe)) + + vector -= swipe + if np.all(np.abs(vector) <= 0): + break + + def full_scan(self, battle_count=None, mystery_count=0): + """Scan the hole map. + + Args: + battle_count: + mystery_count: + """ + logger.info('Full scan start') + + queue = self.map.camera_data + while len(queue) > 0: + if self.map.missing_is_none(battle_count, mystery_count): + logger.info('All spawn found, Early stopped.') + break + queue = queue.sort_by_camera_distance(self.camera) + self.focus_to(queue[0]) + self.predict() + queue = queue[1:] + + if battle_count is not None: + self.map.missing_predict(battle_count=battle_count, mystery_count=mystery_count) + self.map.show() + + def in_sight(self, location, sight=(-3, -1, 3, 2)): + """Make sure location in camera sight + + Args: + location: + sight: + """ + location = location_ensure(location) + logger.info('In sight: %s' % location2node(location)) + + diff = np.array(location) - self.camera + if diff[1] > sight[3]: + y = diff[1] - sight[3] + elif diff[1] < sight[1]: + y = diff[1] - sight[1] + else: + y = 0 + if diff[0] > sight[2]: + x = diff[0] - sight[2] + elif diff[0] < sight[0]: + x = diff[0] - sight[0] + else: + x = 0 + self.focus_to((self.camera[0] + x, self.camera[1] + y)) + + def convert_map_to_grid(self, location): + """If self.grids doesn't contain this location, focus camera on the location and re-convert it. + + Args: + location: Grid instance in self.map. + + Returns: + Grid: Grid instance in self.grids. + """ + location = location_ensure(location) + + grid = np.array(location) - self.camera + self.grids.center_grid + # logger.info('Convert_map_to_grid Failed. Map: %s, Camera: %s, grids_center: %s, grid: %s' % ( + # location2node(location), str(self.camera), str(self.grids.center_grid), str(grid))) + if grid in self.grids: + return self.grids[grid] + else: + logger.warning('Convert_map_to_grid Failed. Map: %s, Camera: %s, grids_center: %s, grid: %s' % ( + location2node(location), str(self.camera), str(self.grids.center_grid), str(grid))) + self.grids.save_error_image() + self.focus_to(location) + grid = np.array(location) - self.camera + self.grids.center_grid + return self.grids[grid] + + def full_scan_find_boss(self): + logger.info('Full scan find boss.') + queue = self.map.select(may_boss=True) + while len(queue) > 0: + queue = queue.sort_by_camera_distance(self.camera) + self.in_sight(queue[0]) + self.predict() + queue = queue[1:] + + boss = self.map.select(is_boss=True) + boss = boss.add(self.map.select(may_boss=True, is_enemy=True)) + if boss: + logger.info(f'Boss found: {boss}') + self.map.show() + return True + + logger.warning('No boss found.') + return False diff --git a/module/map/exception.py b/module/map/exception.py new file mode 100644 index 000000000..2be687088 --- /dev/null +++ b/module/map/exception.py @@ -0,0 +1,2 @@ +class CampaignEnd(Exception): + pass diff --git a/module/map/fleet.py b/module/map/fleet.py new file mode 100644 index 000000000..3ae013a87 --- /dev/null +++ b/module/map/fleet.py @@ -0,0 +1,280 @@ +from module.base.timer import Timer +from module.handler.ambush import AmbushHandler +from module.handler.mystery import MysteryHandler +from module.logger import logger +from module.map.camera import Camera +from module.map.map_base import location2node, location_ensure +from module.map.map_operation import MapOperation + + +class Fleet(Camera, AmbushHandler, MysteryHandler, MapOperation): + fleet_1_location = () + fleet_2_location = () + fleet_current_index = 1 + battle_count = 0 + mystery_count = 0 + fleet_ammo = 5 + ammo_count = 3 + + @property + def fleet_1(self): + if self.fleet_current_index != 1: + self.fleet_switch() + return self + + @fleet_1.setter + def fleet_1(self, value): + self.fleet_1_location = value + + @property + def fleet_2(self): + if self.fleet_current_index != 2: + self.fleet_switch() + return self + + @fleet_2.setter + def fleet_2(self, value): + self.fleet_2_location = value + + @property + def fleet_current(self): + if self.fleet_current_index == 2: + return self.fleet_2_location + else: + return self.fleet_1_location + + def fleet_switch(self): + self.fleet_switch_click() + self.fleet_current_index = 1 if self.fleet_current_index == 2 else 2 + self.camera = self.fleet_current + self.update() + self.find_path_initial() + self.show_fleet() + + def switch_to(self): + pass + + def _goto(self, location, expected=''): + """Goto a grid directly and handle ambush, air raid, mystery picked up, combat. + + Args: + location (tuple, str, GridInfo): Destination. + """ + location = location_ensure(location) + self.in_sight(location, sight=(-3, 0, 3, 2)) + grid = self.convert_map_to_grid(location) + + while 1: + self.ambush_color_initial() + grid.__str__ = location + result = '' + self.device.click(grid) + arrived = False + # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat . + arrive_timer = Timer(0.3) + # Wait after ambushed. + ambushed_retry = Timer(0.5) + # If nothing happens, click again. + walk_timeout = Timer(10) + walk_timeout.start() + + while 1: + self.device.screenshot() + grid.image = self.device.image + + if self.handle_ambush(): + ambushed_retry.start() + # 这个虽然到了之后还会原地再点一次, 但还是先用着, 问题不大 + # arrived = False + # 这个可能会误以为已经到达 + # arrived = grid.predict_fleet() + # 把break去掉就搞定了 + # break + + if self.handle_mystery(button=grid): + # arrived = True + self.mystery_count += 1 + result = 'mystery' + # break + + if self.combat_appear(): + self.combat(expected_end=self._expected_combat_end) + arrived = True + result = 'combat' + self.battle_count += 1 + self.fleet_ammo -= 1 + self.map[location_ensure(location)].is_cleared = True + self.handle_boss_appear_refocus() + grid = self.convert_map_to_grid(location) + # break + + if self.handle_map_cat_attack(): + continue + + if grid.predict_fleet(): + arrive_timer.start() + if not arrive_timer.reached(): + continue + if expected and result != expected: + continue + logger.info('Arrive confirm') + arrived = True + break + + # End + if ambushed_retry.started() and ambushed_retry.reached(): + break + if not arrived and walk_timeout.reached(): + logger.warning('Walk timeout. Retrying.') + break + + # End + if arrived: + break + + self.map[self.fleet_current].is_fleet = False + self.map[location].wipe_out() + self.map[location].is_fleet = True + self.__setattr__('fleet_%s_location' % self.fleet_current_index, location) + + self.find_path_initial() + + def goto(self, location, optimize=True, expected=''): + # self.device.sleep(1000) + location = location_ensure(location) + if self.config.MAP_HAS_AMBUSH and optimize: + nodes = self.map.find_path(location) + for node in nodes: + self._goto(node, expected=expected) + else: + self._goto(location, expected=expected) + + def find_path_initial(self): + self.map.find_path_initial(self.fleet_current) + + def show_fleet(self): + fleets = [] + for n in [1, 2]: + fleet = self.__getattribute__('fleet_%s_location' % n) + if len(fleet): + text = 'Fleet_%s: %s' % (n, location2node(fleet)) + if self.fleet_current_index == n: + text = '[%s]' % text + fleets.append(text) + logger.info(' '.join(fleets)) + + def find_current_fleet(self): + fleets = self.map.select(is_fleet=True, is_spawn_point=True) + logger.info('Fleets: %s' % str(fleets)) + count = fleets.count + if count == 1: + self.fleet_1 = fleets[0].location + elif count == 2: + fleets = fleets.sort_by_camera_distance(self.camera) + self.in_sight(fleets[0], sight=(-1, 0, 1, 2)) + if self.convert_map_to_grid(fleets[0]).predict_current_fleet(): + self.fleet_1 = fleets[0].location + self.fleet_2 = fleets[1].location + else: + self.fleet_1 = fleets[1].location + self.fleet_2 = fleets[0].location + else: + if count == 0: + logger.warning('No fleets detected. Checking fleet spawn points.') + if count > 2: + logger.warning('Too many fleets: %s. Re-checking all spawn points.' % str(fleets)) + queue = self.map.select(is_spawn_point=True) + while queue: + queue = queue.sort_by_camera_distance(self.camera) + self.in_sight(queue[0], sight=(-1, 0, 1, 2)) + grid = self.convert_map_to_grid(queue[0]) + if grid.predict_current_fleet(): + self.fleet_1 = grid.location + elif grid.predict_fleet(): + self.fleet_2 = grid.location + queue = queue[1:] + + self.fleet_current_index = 1 + self.show_fleet() + return self.fleet_current + + def map_init(self, map_): + logger.hr('Map init') + self.battle_count = 0 + self.mystery_count = 0 + self.ammo_count = 3 + self.map = map_ + self.map.reset() + self.ensure_edge_insight(preset=self.map.in_map_swipe_preset_data) + self.full_scan(battle_count=self.battle_count, mystery_count=self.mystery_count) + self.find_current_fleet() + self.find_path_initial() + + @property + def _expected_combat_end(self): + for data in self.map.spawn_data: + if data.get('battle') == self.battle_count and data.get('boss', 0): + return 'in_stage' + if data.get('battle') == self.battle_count + 1: + if data.get('enemy', 0) > 0: + return 'with_searching' + else: + return 'no_searching' + + def fleet_at(self, grid, fleet=None): + """ + Args: + grid (Grid): + fleet (int): 1, 2 + + Returns: + bool: If fleet is at grid. + """ + if fleet is None: + return self.fleet_current == grid.location + if fleet == 1: + return self.fleet_1_location == grid.location + else: + return self.fleet_2_location == grid.location + + def check_accessibility(self, grid, fleet=None): + """ + Args: + grid (Grid): + fleet (int): 1, 2 + + Returns: + bool: If accessible. + """ + if fleet is None: + return grid.is_accessible + if fleet == self.fleet_current_index: + return grid.is_accessible + else: + backup = self.fleet_current_index + self.fleet_current_index = fleet + self.find_path_initial() + result = grid.is_accessible + + self.fleet_current_index = backup + self.find_path_initial() + return result + + def handle_boss_appear_refocus(self): + """ + + """ + appear = False + for data in self.map.spawn_data: + if data.get('battle') == self.battle_count and data.get('boss', 0): + appear = True + + if appear: + logger.info('Catch camera re-positioning after boss appear') + camera = self.camera + self.ensure_edge_insight() + logger.info('Refocus to previous camera position.') + self.focus_to(camera) + return True + else: + return False diff --git a/module/map/grid.py b/module/map/grid.py new file mode 100644 index 000000000..41919a317 --- /dev/null +++ b/module/map/grid.py @@ -0,0 +1,38 @@ +import numpy as np + +from module.base.utils import area_pad +from module.map.grid_info import GridInfo +from module.map.grid_predictor import GridPredictor + + +class Grid(GridPredictor, GridInfo): + def __init__(self, location, image, corner): + """ + + Args: + location(tuple): + image: + corner: (x0, y0) +-------+ (x1, y1) + / \ + / \ + (x2, y2) +-------------+ (x3, y3) + """ + self.location = location + super().__init__(location, image, corner) + self.corner = corner.flatten() + + @property + def button(self): + """ + Returns: + tuple: area + """ + x0, y0, x1, y1, x2, y2, x3, y3 = self.corner + area = tuple(np.rint((max(x0, x2), max(y0, y1), min(x1, x3), min(y2, y3))).astype(int)) + return area_pad(area, pad=25) + + @property + def avoid_area(self): + x0, y0, x1, y1, x2, y2, x3, y3 = self.corner + area = tuple(np.rint((min(x0, x2), min(y0, y1), max(x1, x3), max(y2, y3))).astype(int)) + return area_pad(area, pad=-25) diff --git a/module/map/grid_info.py b/module/map/grid_info.py new file mode 100644 index 000000000..543d2bb53 --- /dev/null +++ b/module/map/grid_info.py @@ -0,0 +1,161 @@ +from module.base.utils import location2node +from module.logger import logger + + +class GridInfo: + """ + Class that gather basic information of a grid in map_v1. + + Visit 碧蓝航线WIKI(Chinese Simplified) http://wiki.joyme.com/blhx, to get basic info of a map_v1. For example, + visit http://wiki.joyme.com/blhx/7-2, to know more about campaign 7-2, which includes boss point, enemy spawn point. + + A grid contains these unchangeable properties which can known from WIKI. + | print_name | property_name | description | + |------------|----------------|-------------------------| + | ++ | is_land | fleet can't go to land | + | -- | is_sea | sea | + | __ | | submarine spawn point | + | SP | is_spawn_point | fleet may spawns here | + | ME | may_enemy | enemy may spawns here | + | MB | may_boss | boss may spawns here | + | MM | may_mystery | mystery may spawns here | + | MA | may_ammo | fleet can get ammo here | + | MS | may_siren | Siren/Elite enemy spawn | + """ + + # is_sea -- + is_land = False # ++ + is_spawn_point = False # SP + + may_enemy = False # ME + may_boss = False # MB + may_mystery = False # MM + may_ammo = False # MA + may_siren = False # MS + + is_enemy = False # example: 0L 1M 2C 3T 3E + is_boss = False # BO + is_mystery = False # MY + is_ammo = False # AM + is_fleet = False # FL + is_submarine = False # SS + is_siren = False # SI + + enemy_scale = 0 + enemy_type = 'Enemy' # Light, Main, Carrier, Treasure, Enemy(unknown) + + is_cleared = False + is_ambush_save = False + cost = 9999 + connection = None + weight = 1 + + location = None + + def decode(self, text): + dic = { + '++': 'is_land', + 'SP': 'is_spawn_point', + 'ME': 'may_enemy', + 'MB': 'may_boss', + 'MM': 'may_mystery', + 'MA': 'may_ammo', + 'MS': 'may_siren', + } + if text in dic: + self.__setattr__(dic[text], True) + if self.may_enemy or self.may_boss or self.may_mystery or self.may_mystery: + self.is_ambush_save = True + if self.may_siren: + self.may_enemy = True + if self.may_boss: + self.may_enemy = True + + def encode(self): + dic = { + '++': 'is_land', + 'BO': 'is_boss', + 'SI': 'is_siren' + } + for key, value in dic.items(): + if self.__getattribute__(value): + return key + + if self.is_enemy: + # if self.may_siren: + # return 'SI' + # else: + return '%s%s' % (self.enemy_scale, self.enemy_type[0].upper()) + + dic = { + 'FL': 'is_fleet', + 'MY': 'is_mystery', + 'AM': 'is_ammo', + '==': 'is_cleared' + } + for key, value in dic.items(): + if self.__getattribute__(value): + return key + + return '--' + + def __str__(self): + return location2node(self.location) + + @property + def str(self): + return self.encode() + + @property + def is_sea(self): + return False if self.is_land or self.is_enemy or self.is_mystery or self.is_boss else True + + @property + def is_accessible(self): + return self.cost < 9999 + + @property + def is_nearby(self): + return self.cost < 20 + + def update(self, info): + """ + Args: + info(GridInfo): + """ + # failure = 0 + for item in ['boss', 'siren']: + if info.__getattribute__('is_' + item): + if self.__getattribute__('may_' + item): + self.__setattr__('is_' + item, True) + return True + else: + logger.info(f'Wrong Prediction. Grid: {self}, Attr: {item}') + # failure += 1 + + if info.is_enemy and self.may_enemy and not self.is_cleared: + self.is_enemy = True + self.enemy_scale = info.enemy_scale + self.enemy_type = info.enemy_type + return True + + for item in ['mystery', 'ammo']: + if info.__getattribute__('is_' + item): + if self.__getattribute__('may_' + item): + self.__setattr__('is_' + item, True) + return True + else: + logger.info(f'Wrong Prediction. Grid: {self}, Attr: {item}') + # failure += 1 + + self.is_fleet = info.is_fleet + return False + + def wipe_out(self): + self.is_enemy = False + self.enemy_scale = 0 + self.enemy_type = 'Enemy' + self.is_mystery = False + self.is_boss = False + self.is_ammo = False + self.is_siren = False diff --git a/module/map/grid_predictor.py b/module/map/grid_predictor.py new file mode 100644 index 000000000..479f57263 --- /dev/null +++ b/module/map/grid_predictor.py @@ -0,0 +1,200 @@ +import os + +import cv2 +import numpy as np +from PIL import Image +from skimage.color import rgb2hsv + +from module.base.utils import color_similarity_2d + + +class Template: + def __init__(self, file, similarity=0.85): + file = os.path.join(TEMPLATE_FOLDER, file) + self.image = np.array(Image.open(file)) + self.similarity = similarity + + def match(self, image): + res = cv2.matchTemplate(np.array(image), self.image, cv2.TM_CCOEFF_NORMED) + _, similarity, _, _ = cv2.minMaxLoc(res) + # print(similarity) + return similarity > self.similarity + + +TEMPLATE_FOLDER = './template' +TEMPLATE_ENEMY_S = Template('ENEMY_S.png', similarity=0.85) +TEMPLATE_ENEMY_M = Template('ENEMY_M.png', similarity=0.85) +TEMPLATE_ENEMY_L = Template('ENEMY_L.png', similarity=0.85) +# TEMPLATE_ENEMY_RED_BORDER = Template('ENEMY_RED_BORDER.png', similarity=0.85) + + +class GridPredictor: + ENEMY_SCALE_IMAGE_SIZE = (50, 50) + ENEMY_PERSPECTIVE_IMAGE_SIZE = (50, 50) + + def __init__(self, location, image, corner): + """ + + Args: + location: + image: + corner: + """ + self.location = location + self.image = image + self.corner = corner.flatten() + + x0, y0, x1, y1, x2, y2, x3, y3 = self.corner + divisor = x0 - x1 + x2 - x3 + x = (x0 * x2 - x1 * x3) / divisor + y = (x0 * y2 - x1 * y2 + x2 * y0 - x3 * y0) / divisor + self._image_center = np.array([x, y, x, y]) + self._image_a = (-x0 * x2 + x0 * x3 + x1 * x2 - x1 * x3) / divisor + self._perspective = ( + (-x0 + x1) / self.ENEMY_PERSPECTIVE_IMAGE_SIZE[0], # a + (-x0 * x3 + x1 * x2) / (-x2 + x3) / self.ENEMY_PERSPECTIVE_IMAGE_SIZE[1], # b + x0, # c + 0, # d + (-x0 * y2 + x1 * y2 + x2 * y0 - x3 * y0) / (-x2 + x3) / self.ENEMY_PERSPECTIVE_IMAGE_SIZE[1], # e + y0, # f + 0, # g + ((-x0 + x1) / (-x2 + x3) - 1) / self.ENEMY_PERSPECTIVE_IMAGE_SIZE[1] # h + ) + self.image_transform = self.image.transform(self.ENEMY_PERSPECTIVE_IMAGE_SIZE, Image.PERSPECTIVE, self._perspective) + + def predict(self): + # self.image_hole = self.get_relative_image((-1, -1, 1, 1)) + self.is_enemy = self.predict_static_red_border() + + self.enemy_scale = self.predict_enemy_scale() + if self.enemy_scale > 0: + self.is_enemy = True + if self.may_siren: + if not self.is_enemy: + self.is_enemy = self.predict_dynamic_red_border() + if self.is_enemy: + self.is_siren = True + self.is_mystery = self.predict_mystery() + self.is_fleet = self.predict_fleet() + self.is_boss = self.predict_boss() + # self.image_perspective = color_similarity_2d( + # self.image.transform(self.ENEMY_PERSPECTIVE_IMAGE_SIZE, Image.PERSPECTIVE, self._perspective) + # , color=(255, 36, 82) + # ) + + def get_relative_image(self, relative_location, output_shape=None): + """ + + Args: + relative_location(tuple): upper_left_x, upper_left_y, bottom_right_x, bottom_right_y + (-1, -1, 1, 1) + output_shape(tuple): (x, y) + + Returns: + PIL.Image.Image + """ + area = self._image_center + np.array(relative_location) * self._image_a + area = tuple(np.rint(area).astype(int)) + image = self.image.crop(area) + if output_shape is not None: + image = image.resize(output_shape) + return image + + def predict_enemy_scale(self): + """ + icon on the upperleft which shows enemy scale: Large Middle Small. + + Returns: + int: 1: Small, 2: Middle, 3: Large. + """ + # if not self.is_enemy: + # return 0 + + image = self.get_relative_image((-0.415 - 0.7, -0.62 - 0.7, -0.415, -0.62)) + image = np.stack( + [ + color_similarity_2d(image, (255, 130, 132)), + color_similarity_2d(image, (255, 239, 148)), + color_similarity_2d(image, (255, 235, 156)) + ], axis=2 + ) + image = Image.fromarray(image).resize(self.ENEMY_SCALE_IMAGE_SIZE) + + if TEMPLATE_ENEMY_L.match(image): + scale = 3 + elif TEMPLATE_ENEMY_M.match(image): + scale = 2 + elif TEMPLATE_ENEMY_S.match(image): + scale = 1 + else: + scale = 0 + + return scale + + def predict_static_red_border(self): + # image = self.image.transform(self.ENEMY_PERSPECTIVE_IMAGE_SIZE, Image.PERSPECTIVE, self._perspective) + + image = color_similarity_2d(self.image_transform, color=(255, 36, 82)) + + # Image.fromarray(np.array(image).astype('uint8'), mode='RGB').save(f'{self}.png') + + count = np.sum(image > 221) + return count > 40 + + def predict_dynamic_red_border(self, pad=4): + image = np.array(self.image_transform).astype(float) + r, b = image[:, :, 0], image[:, :, 2] + image = r - b + image[image < 0] = 0 + image[image > 255] = 255 + + mask = np.ones(np.array(image.shape) - pad * 2) * -1 + mask = np.pad(mask, ((pad, pad), (pad, pad)), mode='constant', constant_values=1) + image = image * mask + image[r < 221] = 0 + return np.mean(image) > 2 + + + def screen_point_to_grid_location(self, point): + a, b, c, d, e, f, g, h = self._perspective + y = (point[1] - f) / (e - point[1] * h) + x = (point[0] * (h * y + 1) - b * y - c) / a + res = np.array((x, y)) / self.ENEMY_PERSPECTIVE_IMAGE_SIZE + return res + + def _relative_image_color_count(self, area, color, output_shape=(50, 50), color_threshold=221): + image = self.get_relative_image(area, output_shape=output_shape) + image = color_similarity_2d(image, color=color) + count = np.sum(image > color_threshold) + return count + + def predict_mystery(self): + # if not self.may_mystery: + # return False + # cyan question mark + if self._relative_image_color_count( + area=(-0.3, -2, 0.3, -0.6), color=(148, 255, 247), output_shape=(20, 50)) > 50: + return True + # white background + # if self._relative_image_color_count( + # area=(-0.7, -1.7, 0.7, -0.3), color=(239, 239, 239), output_shape=(50, 50)) > 700: + # return True + + return False + + def predict_fleet(self): + # white ammo icon + return self._relative_image_color_count( + area=(-1, -2, -0.5, -1.5), color=(255, 255, 255), color_threshold=252) > 300 + + def predict_current_fleet(self): + # Green arrow over head with hue around 141. + image = self.get_relative_image((-0.5, -3.5, 0.5, -2.5)) + hue = rgb2hsv(np.array(image) / 255)[:, :, 0] * 360 + count = np.sum((141 - 3 < hue) & (hue < 141 + 3)) + return count > 1000 + + def predict_boss(self): + count = self._relative_image_color_count( + area=(-0.5, -0.2, 0.5, 0.2), color=(255, 77, 82), color_threshold=247) + return count > 100 diff --git a/module/map/grids.py b/module/map/grids.py new file mode 100644 index 000000000..9132ba751 --- /dev/null +++ b/module/map/grids.py @@ -0,0 +1,89 @@ +import numpy as np + +from module.base.utils import area_in_area +from module.config.config import AzurLaneConfig +from module.logger import logger +from module.map.grid import Grid +from module.map.perspective import Perspective, Lines + + +class Grids(Perspective): + def __init__(self, image, config): + """ + Args: + image: + config(AzurLaneConfig): + """ + self.image = image + self.config = config + # try: + super().__init__(image, config) + + self.grids = {} + for grid in self._gen(): + self.grids[grid.location] = grid + + # Handle offset. + offset = np.min(list(self.grids.keys()), axis=0) + if np.sum(offset) > 0: + logger.info(' Grids offset: %s.' % str(tuple(offset))) + self.vertical = self.vertical[offset[0]:] + self.horizontal = self.horizontal[offset[1]:] + self.grids = {} + for grid in self._gen(): + self.grids[grid.location] = grid + + self.center_grid = ( + np.where(self.vertical.distance_to_point(self.config.SCREEN_CENTER) >= 0)[0][0] - 1, + np.where(self.horizontal.distance_to_point(self.config.SCREEN_CENTER) >= 0)[0][0] - 1 + ) + self.center_offset = self.grids[self.center_grid].screen_point_to_grid_location(self.config.SCREEN_CENTER) + self.shape = np.max(list(self.grids.keys()), axis=0) + + # self.save_error_image() + # except: + # logger.warn('Perspective error') + # pass + # self.save_error_image() + + # def save(self, folder='../screenshot'): + # timestamp = str(int(time.time() * 1000)) + # self.image.save(os.path.join(folder, 'z_%s.png' % timestamp)) + # for grid in self.grids.values(): + # file = os.path.join(folder, '%s_%s_%s.png' % (timestamp, grid.location[0], grid.location[1])) + # grid.image_icon().save(file) + + def __iter__(self): + return iter(self.grids.values()) + + def __getitem__(self, item): + return self.grids[tuple(item)] + + def __contains__(self, item): + return tuple(item) in self.grids + + def _gen(self): + for x, vert in enumerate(zip(self.vertical[:-1], self.vertical[1:])): + for y, hori in enumerate(zip(self.horizontal[:-1], self.horizontal[1:])): + vert = Lines(np.vstack(vert), is_horizontal=False, config=self.config) + hori = Lines(np.vstack(hori), is_horizontal=True, config=self.config) + cross = hori.cross(vert) + area = np.append(cross[0], cross[3]) + + if area_in_area(area, self.config.DETECTING_AREA): + grid = Grid(location=(x, y), image=self.image, corner=cross.points) + yield grid + + def show(self): + for y in range(self.shape[1] + 1): + text = ' '.join([self[(x, y)].str if (x, y) in self else ' ' for x in range(self.shape[0] + 1)]) + logger.info(text) + + def predict(self): + for grid in self: + grid.predict() + + def update(self, image): + self.image = image + for grid in self: + grid.image = image diff --git a/module/map/map.py b/module/map/map.py new file mode 100644 index 000000000..a0134136b --- /dev/null +++ b/module/map/map.py @@ -0,0 +1,303 @@ +from module.logger import logger +from module.map.exception import CampaignEnd +from module.map.fleet import Fleet +from module.map.grid_info import GridInfo +from module.map.map_grids import SelectedGrids, RoadGrids + + +class Map(Fleet): + def clear_chosen_enemy(self, grid): + """ + Args: + grid (GridInfo): + """ + logger.info('Clear enemy: %s' % grid) + self.show_fleet() + if self.config.ENABLE_EMOTION_REDUCE and self.config.ENABLE_MAP_FLEET_LOCK: + self.emotion.wait() + self.goto(grid, expected='combat') + + self.full_scan(battle_count=self.battle_count, mystery_count=self.mystery_count) + self.find_path_initial() + + def clear_chosen_mystery(self, grid): + """ + Args: + grid (GridInfo): + """ + logger.info('Clear mystery: %s' % grid) + self.show_fleet() + self.goto(grid, expected='mystery') + # self.mystery_count += 1 + + def pick_up_ammo(self, grid=None): + """ + Args: + grid (GridInfo): + """ + if grid is None: + grid = self.map.select(may_ammo=True) + if not grid: + logger.warning('Ammo not found') + grid = grid[0] + + if self.ammo_count > 0: + logger.info('Pick up ammo: %s' % grid) + self.goto(grid, expected='') + self.goto(grid, expected='') + self.ensure_no_info_bar() + + # self.ammo_count -= 5 - self.battle_count + recover = 5 - self.fleet_ammo + recover = 3 if recover > 3 else recover + logger.attr('Got ammo', recover) + + self.ammo_count -= recover + self.fleet_ammo += recover + + def sort_grids(self, grids, nearby=False, is_accessible=True, scale=(), strongest=False, weakest=False, cost=False, weight=False, ignore=None): + """ + Args: + grids (SelectedGrids): + nearby: + is_accessible: + scale: + strongest: + weakest: + cost: + weight: + ignore: + + Returns: + SelectedGrids: + """ + # logger.info(f'nearby={nearby}. is_accessible={is_accessible}.') + # logger.info(f'scale={scale}. strongest={strongest}. weakest={weakest}.') + # logger.info(f'cost={cost}. weight={weight}.') + + if nearby: + grids = grids.select(is_nearby=True) + if is_accessible: + grids = grids.select(is_accessible=True) + if ignore is not None: + grids = grids.delete(grids=ignore) + if len(scale): + enemy = SelectedGrids([]) + for enemy_scale in scale: + enemy = enemy.add(grids.select(enemy_scale=enemy_scale)) + grids = enemy + if strongest: + for scale in [3, 2, 1, 0]: + enemy = grids.select(enemy_scale=scale) + if enemy: + grids = enemy + break + if weakest: + for scale in [1, 2, 3, 0]: + enemy = grids.select(enemy_scale=scale) + if enemy: + grids = enemy + break + + if grids: + grids = grids.sort(cost=cost, weight=weight) + + return grids + + + def clear_all_mystery(self, nearby=False, ignore=None): + """Methods to pick up all mystery. + + Args: + nearby (bool): If only walk through ambush save area. + ignore (SelectedGrids): Ignore grids. + + Returns: + bool: False, because didn't clear any enemy. + """ + while 1: + self.map.show_cost() + grids = self.map.select(is_mystery=True, is_accessible=True) + if nearby: + grids = grids.select(is_nearby=True) + if ignore is not None: + grids = grids.delete(grids=ignore) + if not grids: + break + + logger.hr('Clear all mystery') + grids = grids.sort(cost=True, weight=False) + logger.info('Nearby=%s. grids: %s' % (str(nearby), str(grids))) + self.clear_chosen_mystery(grids[0]) + + return False + + def clear_enemy(self, nearby=False, scale=(), strongest=False): + """ + Methods to clear a enemy. May not do anything if no suitable enemy. + Args: + nearby (bool): If only walk through ambush save area. + scale (tuple[int]): Enemy scale. + strongest (bool): If choose the strongest enemy. + + Returns: + bool: True if clear an enemy. + """ + grids = self.map.select(is_enemy=True, is_accessible=True) + if nearby: + grids = grids.select(is_nearby=True) + if len(scale): + enemy = SelectedGrids([]) + for enemy_scale in scale: + enemy = enemy.add(grids.select(enemy_scale=enemy_scale)) + grids = enemy + if strongest: + for scale in [3, 2, 1, 0]: + enemy = grids.select(enemy_scale=scale) + if enemy: + grids = enemy + break + + if grids: + logger.hr('Clear enemy') + grids = grids.sort(cost=True, weight=True) + logger.info(f'Nearby={nearby}. scale={scale}. strongest={strongest}. grids: {grids}') + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_roadblocks(self, roads): + """Clear roadblocks. + + Args: + roads(list[RoadGrids]): + + Returns: + bool: True if clear an enemy. + """ + grids = SelectedGrids([]) + for road in roads: + grids = grids.add(road.roadblocks()) + + grids = grids.select(is_accessible=True) + + if grids: + logger.hr('Clear roadblock') + logger.info('Roadblocks: %s' % str(grids)) + grids = grids.sort(cost=False, weight=True) + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_potential_roadblocks(self, roads, **kwargs): + """Avoid roadblock that only has one grid empty. + + Args: + roads(list[RoadGrids]): + + Returns: + bool: True if clear an enemy. + """ + grids = SelectedGrids([]) + for road in roads: + grids = grids.add(road.potential_roadblocks()) + + grids = grids.select(is_accessible=True) + + if grids: + logger.hr('Avoid potential roadblock') + logger.info(f'Roadblocks potential: {grids}') + grids = grids.sort(cost=False, weight=True) + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_grids_for_faster(self, grids): + """Clear some grids to walk a shorter distance. + + Args: + grids(SelectedGrids): + + Returns: + bool: True if clear an enemy. + """ + + grids = grids.select(is_enemy=True, is_accessible=True) + + if grids: + logger.hr('Clear grids for faster') + grids = grids.sort(cost=True, weight=False) + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_boss(self): + """Clear BOSS. + + Returns: + bool: + """ + grids = self.map.select(is_boss=True, is_accessible=True) + grids = grids.add(self.map.select(may_boss=True, is_enemy=True, is_accessible=True)) + logger.info('May boss: %s' % self.map.select(may_boss=True)) + logger.info('May boss and is enemy: %s' % self.map.select(may_boss=True, is_enemy=True)) + logger.info('Is boss: %s' % self.map.select(is_boss=True)) + + if grids: + logger.hr('Clear BOSS') + grids = grids.sort(cost=True, weight=True) + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + raise CampaignEnd('BOSS Clear.') + + return False + + def clear_siren(self): + grids = self.map.select(may_siren=True, is_enemy=True, is_accessible=True) + + if grids: + logger.hr('Clear siren') + grids = grids.sort(cost=True, weight=True) + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def fleet_2_step_on(self, grids, roadblocks): + """Fleet step on a grid which can reduce the ambush frequency another fleet. + Of course, you can simply use 'self.fleet_2.goto(grid)' and do the same thing. + However, roads can be block by enemy and this method can handle that. + + Args: + grids (SelectedGrids): + roadblocks (list[RoadGrids]): + + Returns: + bool: if clear an enemy. + """ + for grid in grids: + if self.fleet_at(grid=grid, fleet=2): + return False + + for grid in grids: + if grid.is_enemy: + continue + if self.check_accessibility(grid=grid, fleet=2): + logger.info('Fleet_2 step on %s' % grid) + self.fleet_2.goto(grid) + self.fleet_1.switch_to() + return False + else: + logger.info('Fleet_2 step on %s got roadblocks.' % grid) + self.fleet_1.clear_roadblocks(roadblocks) + return True + + return False diff --git a/module/map/map_base.py b/module/map/map_base.py new file mode 100644 index 000000000..4a7420742 --- /dev/null +++ b/module/map/map_base.py @@ -0,0 +1,372 @@ +import numpy as np + +from module.base.utils import location2node, node2location +from module.logger import logger +from module.map.grid_info import GridInfo +from module.map.map_grids import SelectedGrids + + +def location_ensure(location): + if isinstance(location, GridInfo): + return location.location + elif isinstance(location, str): + return node2location(location) + else: + return location + + +def camera_1d(shape, sight): + start, step = abs(sight[0]), sight[1] - sight[0] + 1 + if shape <= start: + out = shape // 2 + else: + out = list(range(start, 26, step)) + out.append(shape - sight[1]) + out = [x for x in set(out) if x <= shape - sight[1]] + return out + + +def camera_2d(shape, sight): + x = camera_1d(shape=shape[0], sight=[sight[0], sight[2]]) + y = camera_1d(shape=shape[1], sight=[sight[1], sight[3]]) + out = np.array(np.meshgrid(x, y)).T.reshape(-1, 2) + return [tuple(c) for c in out] + + +class CampaignMap: + def __init__(self, name=None): + self.name = name + self.grids = {} + self._shape = (0, 0) + self._map_data = '' + self._weight_data = '' + self._block_data = [] + self._spawn_data = [] + self._camera_data = [] + self.in_map_swipe_preset_data = None + + def __iter__(self): + return iter(self.grids.values()) + + def __getitem__(self, item): + """ + Args: + item: + + Returns: + GridInfo: + """ + return self.grids[tuple(item)] + + def __contains__(self, item): + return tuple(item) in self.grids + + @staticmethod + def _parse_text(text): + text = text.strip() + for y, row in enumerate(text.split('\n')): + row = row.strip() + for x, data in enumerate(row.split(' ')): + yield (x, y), data + + @property + def shape(self): + return self._shape + + @shape.setter + def shape(self, scale): + self._shape = node2location(scale.upper()) + + # camera_data can be generate automatically, but it's better to set it manually. + self._camera_data = SelectedGrids(camera_2d(self._shape, sight=(-3, -1, 3, 2))) + + @property + def map_data(self): + return self._map_data + + @map_data.setter + def map_data(self, text): + self._map_data = text + for loca, data in self._parse_text(text): + grid = GridInfo() + grid.location = loca + grid.weight = 10. # weight_data can be generate automatically + grid.decode(data) + self.grids[loca] = grid + + def show(self): + # logger.info('Showing grids:') + logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) + for y in range(self.shape[1] + 1): + text = str(y + 1) + ' ' + ' '.join( + [self[(x, y)].str if (x, y) in self else ' ' for x in range(self.shape[0] + 1)]) + logger.info(text) + + def update(self, grids, camera): + """ + Args: + grids: + camera (tuple): + """ + # failure = 0 + offset = np.array(camera) - np.array(grids.center_grid) + grids.show() + for grid in grids.grids.values(): + loca = tuple(offset + grid.location) + if loca in self.grids: + self.grids[loca].update(grid) + # flag, fail = self.grids[loca].update(grid) + # failure += fail + + # return failure + return True + + def reset(self): + for grid in self: + grid.wipe_out() + grid.is_cleared = False + + @property + def camera_data(self): + """ + Returns: + SelectedGrids: + """ + return self._camera_data + + @camera_data.setter + def camera_data(self, nodes): + """ + Args: + nodes (list): Contains str. + """ + self._camera_data = SelectedGrids([self[node2location(node)] for node in nodes]) + + @property + def spawn_data(self): + return self._spawn_data + + @spawn_data.setter + def spawn_data(self, data_list): + spawn = {'battle': 0, 'enemy': 0, 'mystery': 0, 'siren': 0, 'boss': 0} + # spawn = {'battle': 0, 'enemy': 0, 'mystery': 0, 'boss': 0} + for data in data_list: + spawn['battle'] = data['battle'] + spawn['enemy'] += data.get('enemy', 0) + data.get('siren', 0) + spawn['mystery'] += data.get('mystery', 0) + spawn['siren'] += data.get('siren', 0) + spawn['boss'] += data.get('boss', 0) + self._spawn_data.append(spawn.copy()) + + @property + def weight_data(self): + return self._weight_data + + @weight_data.setter + def weight_data(self, text): + self._weight_data = text + for loca, data in self._parse_text(text): + self[loca].weight = float(data) + + def show_cost(self): + logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) + for y in range(self.shape[1] + 1): + text = str(y + 1) + ' ' + ' '.join( + [str(self[(x, y)].cost).rjust(4) if (x, y) in self else ' ' for x in range(self.shape[0] + 1)]) + logger.info(text) + + def show_connection(self): + logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) + for y in range(self.shape[1] + 1): + text = str(y + 1) + ' ' + ' '.join( + [location2node(self[(x, y)].connection) if (x, y) in self and self[(x, y)].connection else ' ' for x in + range(self.shape[0] + 1)]) + logger.info(text) + + def find_path_initial(self, location, has_ambush=True): + location = location_ensure(location) + + ambush_cost = 10 if has_ambush else 1 + for grid in self: + grid.cost = 9999 + grid.connection = None + self[location].cost = 0 + total = set([grid for grid in self.grids.keys()]) + visited = [location] + visited = set(visited) + + while 1: + new = visited.copy() + for grid in visited: + for arr in np.array([(0, -1), (0, 1), (-1, 0), (1, 0)]): + arr = tuple(arr + grid) + if arr not in total or self[arr].is_land: + continue + cost = 1 if self[arr].is_ambush_save else ambush_cost + cost += self[grid].cost + + if cost < self[arr].cost: + self[arr].cost = cost + self[arr].connection = grid + elif cost == self[arr].cost: + if abs(arr[0] - grid[0]) == 1: + self[arr].connection = grid + if self[arr].is_sea: + new.add(arr) + if len(new) == len(visited): + break + visited = new + + # self.show_cost() + # self.show_connection() + + def _find_path(self, location): + """ + + Args: + location (tuple): + + Returns: + list[tuple]: walking route. + + Examples: + MAP_7_2._find_path(node2location('H2')) + [(2, 2), (3, 2), (4, 2), (5, 2), (6, 2), (6, 1), (7, 1)] # ['C3', 'D3', 'E3', 'F3', 'G3', 'G2', 'H2'] + """ + if self[location].connection is None: + return None + res = [location] + while 1: + location = self[location].connection + if len(res) > 30: + logger.warning('Route too long') + logger.warning(res) + # exit(1) + if location is not None: + res.append(location) + else: + break + res.reverse() + + if len(res) == 0: + logger.warning('No path found. Destination: %s' % str(location)) + return [location, location] + + return res + + def _find_route_node(self, route): + """ + + Args: + route (list[tuple]): list of grids. + + Returns: + list[tuple]: list of walking node. + + Examples: + MAP_7_2._find_route_node([(2, 2), (3, 2), (4, 2), (5, 2), (6, 2), (6, 1), (7, 1)]) + [(6, 2), (7, 1)] + """ + res = [] + diff = np.abs(np.diff(route, axis=0)) + turning = np.diff(diff, axis=0)[:, 0] + indexes = np.where(turning == -1)[0] + 1 + for index in indexes: + grid = route[index] + if not self[grid].is_fleet: + res.append(grid) + else: + if (index > 1) and (index + 1 not in indexes): + res.append(route[index - 1]) + if (index < len(route) - 2) and (index + 1 not in indexes): + res.append(route[index + 1]) + res.append(route[-1]) + + return res + + def find_path(self, location): + location = location_ensure(location) + + path = self._find_path(location) + if path is None or not len(path): + logger.warning('No path found. Return destination.') + return [location] + + logger.info('Path: %s' % '[' + ', ' .join([location2node(grid) for grid in path]) + ']') + path = self._find_route_node(path) + logger.info('Path: %s' % '[' + ', ' .join([location2node(grid) for grid in path]) + ']') + + return path + + def missing_get(self, battle_count, mystery_count=0): + missing = self.spawn_data[battle_count].copy() + may = {'enemy': 0, 'mystery': 0, 'siren': 0,'boss': 0} + missing['enemy'] -= battle_count + missing['mystery'] -= mystery_count + for grid in self: + for attr in may.keys(): + if grid.__getattribute__('is_' + attr): + missing[attr] -= 1 + + for grid in self: + if grid.is_fleet or grid.is_mystery or grid.is_siren: + upper = tuple(np.array(grid.location) + (0, -1)) + if upper in self: + upper = self[upper] + for attr in may.keys(): + if upper.__getattribute__('may_' + attr) and not upper.__getattribute__('is_' + attr): + may[attr] += 1 + + logger.info('missing: %s' % missing) + logger.info('may: %s' % may) + return may, missing + + def missing_is_none(self, battle_count, mystery_count=0): + may, missing = self.missing_get(battle_count, mystery_count) + + for key in may.keys(): + if missing[key] != 0: + return False + + return True + + def missing_predict(self, battle_count, mystery_count=0): + may, missing = self.missing_get(battle_count, mystery_count) + + # predict + for grid in self: + if grid.is_fleet or grid.is_mystery: + upper = tuple(np.array(grid.location) + (0, -1)) + if upper in self: + upper = self[upper] + for attr in may.keys(): + if upper.__getattribute__('may_' + attr) and missing[attr] == may[attr]: + logger.info('Predict %s to be %s' % (location2node(upper.location), attr)) + upper.__setattr__('is_' + attr, True) + + def select(self, **kwargs): + """ + Args: + **kwargs: Attributes of Grid. + + Returns: + SelectedGrids: + """ + result = [] + for grid in self: + flag = True + for k, v in kwargs.items(): + if grid.__getattribute__(k) != v: + flag = False + if flag: + result.append(grid) + + return SelectedGrids(result) + + def flatten(self): + """ + + Returns: + list[GridInfo]: + """ + return self.grids.values() diff --git a/module/map/map_fleet_preparation.py b/module/map/map_fleet_preparation.py new file mode 100644 index 000000000..e99a9e24e --- /dev/null +++ b/module/map/map_fleet_preparation.py @@ -0,0 +1,159 @@ +import numpy as np +from PIL import ImageStat + +from module.base.base import ModuleBase +from module.base.button import Button +from module.base.utils import area_offset +from module.logger import logger +from module.map.assets import * + + +class FleetOperator: + FLEET_BAR_SHAPE_Y = 36 + FLEET_BAR_MARGIN_Y = 6 + FLEET_BAR_ACTIVE_STD = 45 # Active: 67, inactive: 12. + FLEET_IN_USE_STD = 20 # In use 52, not in use (3, 6). + FLEET_PREPARE_OPERATION_SLEEP = (0.25, 0.35) + + def __init__(self, choose, bar, clear, in_use, main): + """ + Args: + choose(Button): + bar(Button): + clear(Button): + """ + self._choose = choose + self._bar = bar + self._clear = clear + self._in_use = in_use + self.main = main + + def __str__(self): + return str(self._choose)[:-7] + + def parse_fleet_bar(self, image): + """ + Args: + image(PIL.Image.Image): Image of fleet choosing bar. + + Returns: + list: List of int. Chosen fleet range from 1 to 6. + """ + result = [] + for index, y in enumerate(range(0, image.size[1], self.FLEET_BAR_SHAPE_Y + self.FLEET_BAR_MARGIN_Y)): + area = (0, y, image.size[0], y + self.FLEET_BAR_SHAPE_Y) + stat = ImageStat.Stat(image.crop(area)) + if np.std(stat.mean, ddof=1) > self.FLEET_BAR_ACTIVE_STD: + result.append(index + 1) + logger.info('Current selected: %s' % str(result)) + return result + + def get_button(self, index): + """ + Args: + index(int): Fleet index, 1-6. + + Returns: + Button: Button instance. + """ + area = area_offset(area=( + 0, + (self.FLEET_BAR_SHAPE_Y + self.FLEET_BAR_MARGIN_Y) * (index - 1), + self._bar.area[2] - self._bar.area[0], + (self.FLEET_BAR_SHAPE_Y + self.FLEET_BAR_MARGIN_Y) * (index - 1) + self.FLEET_BAR_SHAPE_Y + ), offset=(self._bar.area[0:2])) + return Button(area=(), color=(), button=area, name='%s_INDEX_%s' % (str(self._bar), str(index))) + + def allow(self): + return self.main.appear(self._choose) + + def clear(self): + self.main.device.click(self._clear) + self.main.device.sleep(self.FLEET_PREPARE_OPERATION_SLEEP) + + def open(self): + self.main.device.click(self._choose) + self.main.device.sleep(self.FLEET_PREPARE_OPERATION_SLEEP) + self.main.device.screenshot() + + def close(self): + self.main.device.click(self._choose) + self.main.device.sleep(self.FLEET_PREPARE_OPERATION_SLEEP) + self.main.device.screenshot() + + def click(self, index): + self.main.device.click(self.get_button(index)) + self.main.device.sleep(self.FLEET_PREPARE_OPERATION_SLEEP) + self.main.device.screenshot() + + def selected(self): + data = self.parse_fleet_bar(self.main.device.image.crop(self._bar.area)) + return data + + def in_use(self): + image = np.array(self.main.device.image.crop(self._in_use.area).convert('L')) + return np.std(image.flatten(), ddof=1) > self.FLEET_IN_USE_STD + + def ensure_to_be(self, index): + self.open() + if index in self.selected(): + self.close() + else: + self.click(index) + + +class FleetPreparation(ModuleBase): + def fleet_preparation(self): + """Change fleets. + + Returns: + bool: True if changed. + """ + logger.info(f'Using fleet: {[self.config.FLEET_1, self.config.FLEET_2, self.config.SUBMARINE]}') + if self.config.FLEET_CHECKED: + return False + if self.appear(FLEET_PREPARATION_HARD_1) or self.appear(FLEET_PREPARATION_HARD_2): + logger.info('Hard Campaign. No fleet preparation') + return False + + fleet_1 = FleetOperator( + choose=FLEET_1_CHOOSE, bar=FLEET_1_BAR, clear=FLEET_1_CLEAR, in_use=FLEET_1_IN_USE, main=self) + fleet_2 = FleetOperator( + choose=FLEET_2_CHOOSE, bar=FLEET_2_BAR, clear=FLEET_2_CLEAR, in_use=FLEET_2_IN_USE, main=self) + submarine = FleetOperator( + choose=SUBMARINE_CHOOSE, bar=SUBMARINE_BAR, clear=SUBMARINE_CLEAR, in_use=SUBMARINE_IN_USE, main=self) + + # Submarine. + if submarine.allow(): + if self.config.SUBMARINE: + submarine.ensure_to_be(self.config.SUBMARINE) + else: + if submarine.in_use(): + submarine.clear() + + # Not using fleet 2. + if not fleet_2.allow(): + self.config.FLEET_2 = 0 + if not self.config.FLEET_2: + if fleet_2.allow(): + if fleet_2.in_use(): + fleet_2.clear() + fleet_1.ensure_to_be(self.config.FLEET_1) + self.config.FLEET_CHECKED = True + return True + + # Using both fleets. + fleet_1.open() + selected = fleet_1.selected() + if self.config.FLEET_1 in selected and self.config.FLEET_2 in selected: + fleet_1.close() + self.config.FLEET_CHECKED = True + return True + else: + fleet_1.close() + if fleet_2.in_use(): + fleet_2.clear() + fleet_1.ensure_to_be(self.config.FLEET_1) + fleet_2.ensure_to_be(self.config.FLEET_2) + self.config.FLEET_CHECKED = True + return True diff --git a/module/map/map_grids.py b/module/map/map_grids.py new file mode 100644 index 000000000..a09c8e19c --- /dev/null +++ b/module/map/map_grids.py @@ -0,0 +1,195 @@ +import operator + +import numpy as np + + +class SelectedGrids: + def __init__(self, grids): + self.grids = grids + + def __iter__(self): + return iter(self.grids) + + def __getitem__(self, item): + if isinstance(item, int): + return self.grids[item] + else: + return SelectedGrids(self.grids[item]) + + def __contains__(self, item): + return item in self.grids + + def __str__(self): + # return str([str(grid) for grid in self]) + return '[' + ', ' .join([str(grid) for grid in self]) + ']' + + def __len__(self): + return len(self.grids) + + def __bool__(self): + return self.count > 0 + + # def __getattr__(self, item): + # return [grid.__getattribute__(item) for grid in self.grids] + + @property + def location(self): + """ + Returns: + list[tuple]: + """ + return [grid.location for grid in self.grids] + + @property + def cost(self): + """ + Returns: + list[int]: + """ + return [grid.cost for grid in self.grids] + + @property + def weight(self): + """ + Returns: + list[int]: + """ + return [grid.weight for grid in self.grids] + + @property + def count(self): + """ + Returns: + int: + """ + return len(self.grids) + + def select(self, **kwargs): + """ + Args: + **kwargs: Attributes of Grid. + + Returns: + SelectedGrids: + """ + result = [] + for grid in self: + flag = True + for k, v in kwargs.items(): + if grid.__getattribute__(k) != v: + flag = False + if flag: + result.append(grid) + + return SelectedGrids(result) + + def add(self, grids): + """ + Args: + grids(SelectedGrids): + + Returns: + SelectedGrids: + """ + return SelectedGrids(self.grids + grids.grids) + + def delete(self, grids): + """ + Args: + grids(SelectedGrids): + + Returns: + SelectedGrids: + """ + g = [grid for grid in self.grids if grid not in grids] + return SelectedGrids(g) + + def sort(self, cost=True, weight=True): + """ + + Args: + cost (bool): + weight (bool): + + Returns: + + """ + attr = [] + if weight: + attr.append('weight') + if cost: + attr.append('cost') + grids = sorted(self.grids, key=operator.attrgetter(*attr)) + return SelectedGrids(grids) + + def sort_by_camera_distance(self, camera): + """ + Args: + camera (tuple): + + Returns: + SelectedGrids: + """ + location = np.array(self.location) + diff = np.sum(np.abs(np.array(location) - camera), axis=1) + # grids = [x for _, x in sorted(zip(diff, self.grids))] + grids = tuple(np.array(self.grids)[np.argsort(diff)]) + return SelectedGrids(grids) + + +class RoadGrids: + def __init__(self, grids): + """ + Args: + grids (list): + """ + self.grids = [] + for grid in grids: + if isinstance(grid, list): + self.grids.append(SelectedGrids(grids=grid)) + else: + self.grids.append(SelectedGrids(grids=[grid])) + + def __str__(self): + return str(' - '.join([str(grid) for grid in self.grids])) + + def roadblocks(self): + """ + Returns: + SelectedGrids: + """ + grids = [] + for block in self.grids: + if block.count == block.select(is_enemy=True).count: + grids += block.grids + return SelectedGrids(grids) + + def potential_roadblocks(self): + """ + Returns: + SelectedGrids: + """ + grids = [] + for block in self.grids: + if np.any([grid.is_fleet for grid in block]): + continue + if np.any([grid.is_cleared for grid in block]): + continue + if block.count - block.select(is_enemy=True).count == 1: + grids += block.select(is_enemy=True).grids + return SelectedGrids(grids) + + def first_roadblock(self): + """ + Returns: + SelectedGrids: + """ + grids = [] + for block in self.grids: + if np.any([grid.is_fleet for grid in block]): + continue + if np.any([grid.is_cleared for grid in block]): + continue + if block.select(is_enemy=True).count == 1: + grids += block.select(is_enemy=True).grids + return SelectedGrids(grids) diff --git a/module/map/map_operation.py b/module/map/map_operation.py new file mode 100644 index 000000000..fb75274d6 --- /dev/null +++ b/module/map/map_operation.py @@ -0,0 +1,161 @@ +from module.base.timer import Timer +from module.handler.enemy_searching import EnemySearchingHandler +from module.handler.urgent_commission import UrgentCommissionHandler +from module.logger import logger +from module.map.assets import * +from module.map.exception import CampaignEnd +from module.map.map_fleet_preparation import FleetPreparation +from module.retire.retirement import Retirement + + +class MapOperation(UrgentCommissionHandler, EnemySearchingHandler, FleetPreparation, Retirement): + def fleet_switch_click(self): + """ + Switch fleet. + """ + logger.info('Switch over') + if self.appear_then_click(SWITCH_OVER): + pass + else: + logger.warning('No buttons detected.') + + self.device.sleep((1, 1.5)) + # self.ensure_no_info_bar() + + def enter_map(self, button, mode='normal'): + """Enter a campaign. + + Args: + button: Campaign to enter. + mode (str): 'normal' or 'hard' or 'cd' + """ + logger.hr('Enter map') + campaign_timer = Timer(2) + map_timer = Timer(1) + fleet_timer = Timer(1) + checked_in_map = False + while 1: + self.device.screenshot() + + if not checked_in_map and self.is_in_map(): + logger.info('Already in map.') + return False + else: + checked_in_map = True + + # Enter campaign + if campaign_timer.reached() and self.appear_then_click(button): + campaign_timer.reset() + continue + + # Map preparation + if map_timer.reached() and self.appear(MAP_PREPARATION): + self.device.click(MAP_PREPARATION) + map_timer.reset() + campaign_timer.reset() + continue + + # Fleet preparation + if fleet_timer.reached() and self.appear(FLEET_PREPARATION): + if self.config.ENABLE_FLEET_CONTROL: + if mode == 'normal' or mode == 'hard': + self.fleet_preparation() + self.device.click(FLEET_PREPARATION) + fleet_timer.reset() + campaign_timer.reset() + continue + + # Retire + if self.handle_retirement(): + continue + + # Emotion + pass + + # Urgent commission + if self.handle_urgent_commission(): + continue + + # End + if self.handle_in_map_with_enemy_searching(): + break + + return True + + def withdraw(self): + """ + Withdraw campaign. + """ + logger.hr('Map withdraw') + while 1: + self.device.screenshot() + + if self.appear_then_click(WITHDRAW, interval=2): + continue + if self.appear_then_click(WITHDRAW_CONFIRM, offset=True, interval=2): + continue + + # End + if self.handle_in_stage(): + raise CampaignEnd('Withdraw') + + def handle_map_cat_attack(self): + """ + Click to skip the animation when cat attacks. + """ + if self.appear_then_click(MAP_CAT_ATTACK, genre='cat_attack', interval=2): + logger.info('Skip map cat attack') + return True + + return False + + def handle_map_fleet_lock(self, lock=None, skip_first_screenshot=True): + """ + Args: + lock (bool, optional): Set to lock or unlock. + skip_first_screenshot (bool): + + Returns: + bool: If fleet lock changed. + """ + if lock is None: + lock = self.config.ENABLE_MAP_FLEET_LOCK + + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if lock: + if self.appear(FLEET_LOCKED): + logger.attr('Map_fleet', 'locked') + return False + elif self.appear_then_click(FLEET_UNLOCKED, interval=1): + continue + + else: + if self.appear(FLEET_UNLOCKED): + logger.attr('Map_fleet', 'unlocked') + return False + elif self.appear_then_click(FLEET_LOCKED, interval=1): + continue + + return True + + def handle_fleet_reverse(self): + """ + The game chooses the fleet with a smaller index to be the first fleet, + no matter what we choose in fleet preparation. + + Returns: + bool: Fleet changed + """ + if (self.config.FLEET_2 == 0) or (self.config.FLEET_2 > self.config.FLEET_1): + return False + + self.fleet_switch_click() + return True + + def handle_spare_fleet(self): + pass diff --git a/module/map/perspective.py b/module/map/perspective.py new file mode 100644 index 000000000..9564d0c0a --- /dev/null +++ b/module/map/perspective.py @@ -0,0 +1,387 @@ +import os +import time +import warnings + +import cv2 +import numpy as np +from PIL import Image, ImageOps, ImageDraw +from scipy import signal, optimize + +from module.config.config import AzurLaneConfig +from module.logger import logger +from module.map.perspective_items import Points, Lines + +warnings.filterwarnings("ignore") + + +class Perspective: + def __init__(self, image, config): + """ + Args: + image: Screenshot + config (AzurLaneConfig): + """ + self.image = image + self.config = config + self.correct = True + start_time = time.time() + + # Image initialisation + image = self.load_image(image) + + # Lines detection + inner_h = self.detect_lines( + image, + is_horizontal=True, + param=self.config.INTERNAL_LINES_FIND_PEAKS_PARAMETERS, + threshold=self.config.INTERNAL_LINES_HOUGHLINES_THRESHOLD, + theta=self.config.HORIZONTAL_LINES_THETA_THRESHOLD + ).move(*self.config.DETECTING_AREA[:2]) + inner_v = self.detect_lines( + image, + is_horizontal=False, + param=self.config.INTERNAL_LINES_FIND_PEAKS_PARAMETERS, + threshold=self.config.INTERNAL_LINES_HOUGHLINES_THRESHOLD, + theta=self.config.VERTICAL_LINES_THETA_THRESHOLD + ).move(*self.config.DETECTING_AREA[:2]) + edge_h = self.detect_lines( + image, + is_horizontal=True, + param=self.config.EDGE_LINES_FIND_PEAKS_PARAMETERS, + threshold=self.config.EDGE_LINES_HOUGHLINES_THRESHOLD, + theta=self.config.HORIZONTAL_LINES_THETA_THRESHOLD, + pad=self.config.DETECTING_AREA[2] - self.config.DETECTING_AREA[0] + ).move(*self.config.DETECTING_AREA[:2]) + edge_v = self.detect_lines( + image, + is_horizontal=False, + param=self.config.EDGE_LINES_FIND_PEAKS_PARAMETERS, + threshold=self.config.EDGE_LINES_HOUGHLINES_THRESHOLD, + theta=self.config.VERTICAL_LINES_THETA_THRESHOLD, + pad=self.config.DETECTING_AREA[3] - self.config.DETECTING_AREA[1] + ).move(*self.config.DETECTING_AREA[:2]) + + # Lines pre-cleansing + # horizontal = inner_h.add(edge_h).group() + # vertical = inner_v.add(edge_v).group() + # edge_h = edge_h.group() + # edge_v = edge_v.group() + horizontal = inner_h.add(edge_h).group() + vertical = inner_v.add(edge_v).group() + edge_h = edge_h.group().delete(inner_h) # Experimental, reduce edge lines. + edge_v = edge_v.group().delete(inner_v) + self.horizontal = horizontal + self.vertical = vertical + + # Calculate perspective + self.crossings = self.horizontal.cross(self.vertical) + self.vanish_point = optimize.brute(self._vanish_point_value, self.config.VANISH_POINT_RANGE) + distance_point_x = optimize.brute(self._distant_point_value, self.config.DISTANCE_POINT_X_RANGE)[0] + self.distant_point = np.array([distance_point_x, self.vanish_point[1]]) + + # Re-generate lines. Useless after mid_cleanse function added. + # self.horizontal = self.crossings.link(None, is_horizontal=True).group() + # self.vertical = self.crossings.link(self.vanish_point).group() + # self.draw(self.crossings.link(self.distant_point)) + # print(edge_h) + # print(inner_h.group()) + + # Lines cleansing + # self.draw(edge_h) + self.horizontal, self.lower_edge, self.upper_edge = self.line_cleanse( + self.horizontal, inner=inner_h.group(), edge=edge_h) + self.vertical, self.left_edge, self.right_edge = self.line_cleanse( + self.vertical, inner=inner_v.group(), edge=edge_v) + + # self.draw() + # print(self.horizontal) + # print(self.lower_edge, self.upper_edge) + # print(self.vertical) + # print(self.left_edge, self.right_edge) + + # Log + time_cost = round(time.time() - start_time, 3) + logger.info('%ss %s Horizontal: %s (%s inner, %s edge)' % ( + str(time_cost).ljust(5, '0'), '_' if self.lower_edge else ' ', + len(self.horizontal), len(horizontal), len(edge_h)) + ) + logger.info('Edges: %s%s%s Vertical: %s (%s inner, %s edge)' % ( + '/' if self.left_edge else ' ', '_' if self.upper_edge else ' ', + '\\' if self.right_edge else ' ', len(self.vertical), len(vertical), len(edge_v)) + ) + if len(horizontal) - len(self.horizontal) >= 3 or len(vertical) - len(self.vertical) >= 3: + logger.warning('Too many deleted lines') + # self.save_error_image() + + def load_image(self, image): + """Method that turns image to monochrome and hide UI. + + Args: + image: Screenshot. + + Returns: + np.ndarray + """ + image = np.array(image.crop(self.config.DETECTING_AREA)) + image = 255 - ((np.max(image, axis=2) // 2 + np.min(image, axis=2) // 2) & self.config.UI_MASK) + return image + + def find_peaks(self, image, is_horizontal, param, pad=0): + """ + Args: + image(np.ndarray): Processed screenshot. + is_horizontal(bool): True if detects horizontal lines. + param(dict): Parameters use in scipy.signal.find_peaks. + pad(int): + + Returns: + np.ndarray: + """ + if is_horizontal: + image = image.T + if pad: + image = np.pad(image, ((0, 0), (0, pad)), mode='constant', constant_values=255) + origin_shape = image.shape + out = np.zeros(origin_shape[0] * origin_shape[1], dtype='uint8') + peaks, _ = signal.find_peaks(image.ravel(), **param) + out[peaks] = 255 + out = out.reshape(origin_shape) + if pad: + out = out[:, :-pad] + if is_horizontal: + out = out.T + out &= self.config.UI_MASK + return out + + def hough_lines(self, image, is_horizontal, threshold, theta): + """ + + Args: + image (np.ndarray): Peaks image. + is_horizontal (bool): True if detects horizontal lines. + threshold (int): Threshold use in cv2.HoughLines + theta: + + Returns: + Lines: + """ + lines = cv2.HoughLines(image, 1, np.pi / 180, threshold) + if lines is None: + return Lines(None, is_horizontal=is_horizontal, config=self.config) + else: + lines = lines[:, 0, :] + if is_horizontal: + lines = lines[(np.deg2rad(90 - theta) < lines[:, 1]) & (lines[:, 1] < np.deg2rad(90 + theta))] + else: + lines = lines[(lines[:, 1] < np.deg2rad(theta)) | (np.deg2rad(180 - theta) < lines[:, 1])] + lines = [[-rho, theta - np.pi] if rho < 0 else [rho, theta] for rho, theta in lines] + # if len(lines) > 0: + # return Lines(lines, is_horizontal=is_horizontal, config=self.config) + return Lines(lines, is_horizontal=is_horizontal, config=self.config) + + def detect_lines(self, image, is_horizontal, param, threshold, theta, pad=0): + """ + Method that wraps find_peaks and hough_lines + """ + peaks = self.find_peaks(image, is_horizontal=is_horizontal, param=param, pad=pad) + # self.show_array(peaks) + lines = self.hough_lines(peaks, is_horizontal=is_horizontal, threshold=threshold, theta=theta) + # self.draw(lines, Image.fromarray(peaks.astype(np.uint8), mode='L')) + return lines + + @staticmethod + def show_array(arr): + image = Image.fromarray(arr.astype(np.uint8), mode='L') + image.show() + + def draw(self, lines=None, bg=None, expend=0): + if bg is None: + image = self.image.copy() + else: + image = bg.copy() + if expend: + image = ImageOps.expand(image, border=expend, fill=0) + draw = ImageDraw.Draw(image) + if lines is None: + lines = self.horizontal.add(self.vertical) + for rho, theta in zip(lines.rho, lines.theta): + a = np.cos(theta) + b = np.sin(theta) + x0 = a * rho + y0 = b * rho + x1 = int(x0 + 10000 * (-b)) + expend + y1 = int(y0 + 10000 * a) + expend + x2 = int(x0 - 10000 * (-b)) + expend + y2 = int(y0 - 10000 * a) + expend + draw.line([x1, y1, x2, y2], 'white') + + image.show() + # image.save('123.png') + + def _vanish_point_value(self, point): + """Value that measures how close a point to the perspective vanish point. The smaller the better. + Use log10 to encourage a group of coincident lines and discourage wrong lines. + + Args: + point(np.ndarray): np.array([x, y]) + + Returns: + float: value. + """ + # Add 0.001 to avoid log10(0). + distance = np.sum(np.log10(np.abs(self.vertical.distance_to_point(point)) + 0.001)) + return distance + + def _distant_point_value(self, x): + """Value that measures how close a point to the perspective distant point. The smaller the better. + Use log10 to encourage a group of coincident lines and discourage wrong lines. + + Args: + x(np.ndarray): np.array([x]) + + Returns: + float: value + """ + links = self.crossings.link((x[0], self.vanish_point[1])) + mid = np.sort(links.mid) + distance = np.sum(np.log10(np.diff(mid) + 0.001)) # Add 0.001 to avoid log10(0). + return distance + + def mid_cleanse(self, mids, is_horizontal, threshold=3): + """ + Args: + mids(np.ndarray): Lines.mid + is_horizontal(bool): True if detects horizontal lines. + threshold(int): + + Returns: + np.ndarray + """ + right_distant_point = (self.vanish_point[0] * 2 - self.distant_point[0], self.distant_point[1]) + + def convert_to_x(ys): + return Points([[self.config.SCREEN_CENTER[0], y] for y in ys], config=self.config) \ + .link(right_distant_point) \ + .mid + + def convert_to_y(xs): + return Points([[x, self.config.SCREEN_CENTER[1]] for x in xs], config=self.config) \ + .link(right_distant_point) \ + .get_y(x=self.config.SCREEN_CENTER[0]) + + def coincident_point_value(point): + """Value that measures how close a point to the coincident point. The smaller the better. + Coincident point may be many. + Use an activation function to encourage a group of coincident lines and ignore wrong lines. + """ + x, y = point + # Do not use: + # distance = coincident.distance_to_point(point) + distance = np.abs(x - coincident.get_x(y)) + # print((distance * 1).astype(int).reshape(len(mids), np.diff(self.config.ERROR_LINES_TOLERANCE)[0]+1)) + + # Activation function + # distance = 1 / (1 + np.exp(16 / distance - distance)) + distance = 1 / (1 + np.exp(9 / distance) / distance) + distance = np.sum(distance) + return distance + + if is_horizontal: + mids = convert_to_x(mids) + + # Drawing lines + lines = [] + for index, mid in enumerate(mids): + for n in range(self.config.ERROR_LINES_TOLERANCE[0], self.config.ERROR_LINES_TOLERANCE[1] + 1): + theta = np.arctan(index + n) + rho = mid * np.cos(theta) + lines.append([rho, theta]) + # Fitting mid + coincident = Lines(np.vstack(lines), is_horizontal=False, config=self.config) + # print(np.round(np.sort(coincident.get_x(128))).astype(int)) + coincident_point = optimize.brute(coincident_point_value, self.config.COINCIDENT_POINT_RANGE) + # print(coincident_point, is_horizontal) + + diff = abs(coincident_point[1] - 129) + if diff > 3: + self.correct = False + logger.warning('%s coincident point unexpected: %s' % ( + 'Horizontal' if is_horizontal else 'Vertical', + str(coincident_point))) + if diff > 6: + self.save_error_image() + + # The limits of detecting area + if is_horizontal: + border = Points( + [[self.config.SCREEN_CENTER[0], self.config.DETECTING_AREA[1]], + [self.config.SCREEN_CENTER[0], self.config.DETECTING_AREA[3]]], config=self.config) \ + .link(right_distant_point) \ + .mid + else: + border = Points( + [self.config.DETECTING_AREA[0:2], self.config.DETECTING_AREA[1:3][::-1]], config=self.config) \ + .link(self.vanish_point) \ + .mid + + left, right = border + # print(mids) + # Filling mid + mids = np.arange(-25, 25) * coincident_point[1] + coincident_point[0] + mids = mids[(mids > left - threshold) & (mids < right + threshold)] + # print(mids) + if is_horizontal: + mids = convert_to_y(mids) + + return mids + + def line_cleanse(self, lines, inner, edge, threshold=3): + origin = lines.mid + clean = self.mid_cleanse(origin, is_horizontal=lines.is_horizontal, threshold=threshold) + + # Cleansing edge + edge = edge.mid + inner = inner.mid + edge = edge[(edge > np.max(inner) - threshold) | (edge < np.min(inner) + threshold)] + edge = [c for c in clean if np.any(np.abs(c - edge) < 5)] + + # Separate edges + if len(edge) == 0: + lower, upper = None, None + elif len(edge) == 1: + edge = edge[0] + if lines.is_horizontal: + lower, upper = (None, edge) if edge > self.config.SCREEN_CENTER[1] else (edge, None) + else: + lower, upper = (None, edge) if edge > self.config.SCREEN_CENTER[0] else (edge, None) + else: + lower, upper = edge[0], edge[-1] + + # crop mid + if lower: + clean = clean[clean > lower - threshold] + if upper: + clean = clean[clean < upper + threshold] + + # mid to lines + if lines.is_horizontal: + lines = Points([[self.config.SCREEN_CENTER[0], y] for y in clean], config=self.config) \ + .link(None, is_horizontal=True) + lower = Points([self.config.SCREEN_CENTER[0], lower], config=self.config).link(None, is_horizontal=True) \ + if lower else Lines(None, config=self.config, is_horizontal=True) + upper = Points([self.config.SCREEN_CENTER[0], upper], config=self.config).link(None, is_horizontal=True) \ + if upper else Lines(None, config=self.config, is_horizontal=True) + else: + lines = Points([[x, self.config.SCREEN_CENTER[1]] for x in clean], config=self.config) \ + .link(self.vanish_point) + lower = Points([lower, self.config.SCREEN_CENTER[1]], config=self.config).link(self.vanish_point) \ + if lower else Lines(None, config=self.config, is_horizontal=False) + upper = Points([upper, self.config.SCREEN_CENTER[1]], config=self.config).link(self.vanish_point) \ + if upper else Lines(None, config=self.config, is_horizontal=False) + + return lines, lower, upper + + def save_error_image(self): + file = '%s.%s' % (int(time.time() * 1000), 'png') + file = os.path.join(self.config.ERROR_LOG_FOLDER, file) + self.image.save(file) diff --git a/module/map/perspective_items.py b/module/map/perspective_items.py new file mode 100644 index 000000000..225269e6d --- /dev/null +++ b/module/map/perspective_items.py @@ -0,0 +1,183 @@ +import numpy as np + + +class Points: + def __init__(self, points, config): + if points is None: + self._bool = False + self.points = None + else: + self._bool = True + self.points = np.array(points) + if len(self.points.shape) == 1: + self.points = np.array([self.points]) + self.config = config + self.x, self.y = self.points.T + + def __str__(self): + return str(self.points) + + def __iter__(self): + return iter(self.points) + + def __getitem__(self, item): + return self.points[item] + + def __len__(self): + return len(self.points) + + def __bool__(self): + return self._bool + + def link(self, point, is_horizontal=False): + if is_horizontal: + lines = [[y, np.pi/2] for y in self.y] + return Lines(lines, is_horizontal=True, config=self.config) + else: + x, y = point + theta = -np.arctan((self.x - x) / (self.y - y)) + rho = self.x * np.cos(theta) + self.y * np.sin(theta) + lines = np.array([rho, theta]).T + return Lines(lines, is_horizontal=False, config=self.config) + + +class Lines: + def __init__(self, lines, is_horizontal, config): + if lines is None or len(lines) == 0: + self._bool = False + self.lines = None + else: + self._bool = True + self.lines = np.array(lines) + if len(self.lines.shape) == 1: + self.lines = np.array([self.lines]) + self.rho, self.theta = self.lines.T + self.is_horizontal = is_horizontal + self.config = config + + def __str__(self): + return str(self.lines) + + def __iter__(self): + return iter(self.lines) + + def __getitem__(self, item): + return Lines(self.lines[item], is_horizontal=self.is_horizontal, config=self.config) + + def __len__(self): + if self: + return len(self.lines) + else: + return 0 + + def __bool__(self): + return self._bool + + @property + def sin(self): + return np.sin(self.theta) + + @property + def cos(self): + return np.cos(self.theta) + + @property + def mean(self): + if not self: + return None + if self.is_horizontal: + return np.mean(self.lines, axis=0) + else: + x = np.mean(self.mid) + theta = np.mean(self.theta) + rho = x * np.cos(theta) + self.config.MID_Y * np.sin(theta) + return np.array((rho, theta)) + + @property + def mid(self): + if not self: + return np.array([]) + if self.is_horizontal: + return self.rho + else: + return (self.rho - self.config.MID_Y * self.sin) / self.cos + + def get_x(self, y): + return (self.rho - y * self.sin) / self.cos + + def get_y(self, x): + return (self.rho - x * self.cos) / self.sin + + def add(self, other): + if not other: + return self + lines = np.append(self.lines, other.lines, axis=0) + return Lines(lines, is_horizontal=self.is_horizontal, config=self.config) + + def move(self, x, y): + if not self: + return self + if self.is_horizontal: + self.lines[:, 0] += y + else: + self.lines[:, 0] += x * self.cos + y * self.sin + return Lines(self.lines, is_horizontal=self.is_horizontal, config=self.config) + + def sort(self): + if not self: + return self + lines = self.lines[np.argsort(self.mid)] + return Lines(lines, is_horizontal=self.is_horizontal, config=self.config) + + def group(self, threshold=3): + if not self: + return self + lines = self.sort() + prev = 0 + regrouped = [] + group = [] + for mid, line in zip(lines.mid, lines.lines): + line = line.tolist() + if mid - prev > threshold: + if len(regrouped) == 0: + if len(group) != 0: + regrouped = [group] + else: + regrouped += [group] + group = [line] + else: + group.append(line) + prev = mid + regrouped += [group] + regrouped = np.vstack([Lines(r, is_horizontal=self.is_horizontal, config=self.config).mean for r in regrouped]) + return Lines(regrouped, is_horizontal=self.is_horizontal, config=self.config) + + def distance_to_point(self, point): + x, y = point + return self.rho - x * self.cos - y * self.sin + + @staticmethod + def cross_two_lines(lines1, lines2): + for rho1, sin1, cos1 in zip(lines1.rho, lines1.sin, lines1.cos): + for rho2, sin2, cos2 in zip(lines2.rho, lines2.sin, lines2.cos): + a = np.array([[cos1, sin1], [cos2, sin2]]) + b = np.array([rho1, rho2]) + yield np.linalg.solve(a, b) + + def cross(self, other): + points = np.vstack(self.cross_two_lines(self, other)) + points = Points(points, config=self.config) + return points + + def delete(self, other, threshold=3): + if not self: + return self + + other_mid = other.mid + lines = [] + for mid, line in zip(self.mid, self.lines): + if np.any(np.abs(other_mid - mid) < threshold): + continue + lines.append(line) + + return Lines(lines, is_horizontal=self.is_horizontal, config=self.config) diff --git a/module/map/ui_mask.png b/module/map/ui_mask.png new file mode 100644 index 000000000..3837c5a30 Binary files /dev/null and b/module/map/ui_mask.png differ diff --git a/module/retire/assets.py b/module/retire/assets.py new file mode 100644 index 000000000..6d2c4a00a --- /dev/null +++ b/module/retire/assets.py @@ -0,0 +1,18 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +EQUIP_CONFIRM = Button(area=(871, 516, 1044, 573), color=(95, 143, 203), button=(871, 516, 1044, 573), file='./assets/retire/EQUIP_CONFIRM.png') +EQUIP_CONFIRM_2 = Button(area=(720, 541, 893, 598), color=(94, 142, 202), button=(720, 541, 893, 598), file='./assets/retire/EQUIP_CONFIRM_2.png') +GET_ITEMS_1_RETIREMENT_SAVE = Button(area=(951, 654, 987, 687), color=(64, 62, 69), button=(951, 654, 987, 687), file='./assets/retire/GET_ITEMS_1_RETIREMENT_SAVE.png') +IN_RETIREMENT_CHECK = Button(area=(854, 641, 1027, 698), color=(184, 99, 89), button=(854, 641, 1027, 698), file='./assets/retire/IN_RETIREMENT_CHECK.png') +RETIRE_APPEAR_1 = Button(area=(353, 492, 527, 550), color=(96, 144, 204), button=(353, 492, 527, 550), file='./assets/retire/RETIRE_APPEAR_1.png') +RETIRE_APPEAR_2 = Button(area=(553, 492, 727, 550), color=(94, 143, 204), button=(553, 492, 727, 550), file='./assets/retire/RETIRE_APPEAR_2.png') +RETIRE_APPEAR_3 = Button(area=(753, 492, 927, 550), color=(94, 143, 202), button=(753, 492, 927, 550), file='./assets/retire/RETIRE_APPEAR_3.png') +SHIP_CONFIRM = Button(area=(1069, 641, 1241, 698), color=(84, 131, 190), button=(1069, 641, 1241, 698), file='./assets/retire/SHIP_CONFIRM.png') +SHIP_CONFIRM_2 = Button(area=(928, 617, 1100, 674), color=(95, 143, 203), button=(928, 617, 1100, 674), file='./assets/retire/SHIP_CONFIRM_2.png') +SORTNG_CLICK = Button(area=(1004, 14, 1096, 42), color=(49, 54, 67), button=(1004, 14, 1096, 42), file='./assets/retire/SORTNG_CLICK.png') +SORT_ASC = Button(area=(1014, 22, 1019, 26), color=(189, 207, 231), button=(1014, 22, 1019, 26), file='./assets/retire/SORT_ASC.png') +SORT_DESC = Button(area=(1014, 29, 1019, 33), color=(189, 207, 231), button=(1014, 29, 1019, 33), file='./assets/retire/SORT_DESC.png') diff --git a/module/retire/retirement.py b/module/retire/retirement.py new file mode 100644 index 000000000..b1a9b433b --- /dev/null +++ b/module/retire/retirement.py @@ -0,0 +1,194 @@ +from module.base.button import ButtonGrid +from module.base.utils import get_color, color_similar +from module.combat.assets import GET_ITEMS_1 +from module.handler.info_bar import InfoBarHandler +from module.logger import logger +from module.retire.assets import * +from module.ui.ui import UI, BACK_ARROW + +CARD_GRIDS = ButtonGrid(origin=(93, 76), delta=(164 + 2 / 3, 227), button_shape=(138, 204), grid_shape=(7, 2), name='CARD') +CARD_RARITY_GRIDS = ButtonGrid(origin=(93, 76), delta=(164 + 2 / 3, 227), button_shape=(138, 5), grid_shape=(7, 2), name='RARITY') + +CARD_RARITY_COLORS = { + 'N': (174, 176, 187), + 'R': (106, 195, 248), + 'SR': (151, 134, 254), + 'SSR': (248, 223, 107) + # Not support marriage cards. +} + +class Retirement(UI, InfoBarHandler): + def _handle_retirement_cards_loading(self): + self.device.sleep((1, 1.5)) + + def _retirement_choose(self, amount=10, target_rarity=('N',)): + """ + Args: + amount (int): Amount of cards retire. 0 to 10. + target_rarity (tuple(str)): Card rarity. N, R, SR, SSR. + + Returns: + int: Amount of cards have retired. + """ + cards = [] + rarity = [] + for x, y, button in CARD_RARITY_GRIDS.generate(): + card_color = get_color(image=self.device.image, area=button.area) + f = False + for r, rarity_color in CARD_RARITY_COLORS.items(): + + if color_similar(card_color, rarity_color, threshold=15): + cards.append([x, y]) + rarity.append(r) + f = True + + if not f: + logger.warning(f'Unknown rarity color. Grid: ({x}, {y}). Color: {card_color}') + + logger.info(' '.join([r.rjust(3) for r in rarity[:7]])) + logger.info(' '.join([r.rjust(3) for r in rarity[7:]])) + + selected = 0 + for card, r in zip(cards, rarity): + if r in target_rarity: + self.device.click(CARD_GRIDS[card]) + self.device.sleep((0.1, 0.15)) + selected += 1 + if selected >= amount: + break + return selected + + def _retirement_set_sort_method(self, method): + """ + Args: + method (str): ASC for ascending, DESC for descending. + + Returns: + bool: If method change. + """ + current = 'UNKNOWN' + self.device.screenshot() + if self.appear(SORT_ASC): + current = 'ASC' + if self.appear(SORT_DESC): + current = 'DESC' + + logger.info(f'Current sorting: {current}') + if current != method: + logger.info(f'Sorting set to {method}') + self.device.click(SORTNG_CLICK) + self._handle_retirement_cards_loading() + self.device.screenshot() + return True + else: + return False + + def _retirement_confirm(self): + executed = False + while 1: + self.device.screenshot() + if self.appear_then_click(SHIP_CONFIRM, offset=30, interval=2): + continue + if self.appear_then_click(SHIP_CONFIRM_2, offset=30, interval=2): + continue + if self.appear_then_click(EQUIP_CONFIRM, offset=30, interval=2): + continue + if self.appear_then_click(EQUIP_CONFIRM_2, offset=30, interval=2): + executed = True + continue + if self.appear(GET_ITEMS_1, interval=2): + self.device.click(GET_ITEMS_1_RETIREMENT_SAVE) + self.interval_reset(SHIP_CONFIRM) + continue + + # End + if executed and self.appear(IN_RETIREMENT_CHECK): + # self._handle_retirement_cards_loading() + # self.device.screenshot() + self.handle_info_bar() + self.device.screenshot() + break + + def retirement_appear(self): + return self.appear(RETIRE_APPEAR_1, offset=30) \ + and self.appear(RETIRE_APPEAR_2, offset=30) \ + and self.appear(RETIRE_APPEAR_3, offset=30) + + def _retirement_quit(self): + skip = True + while 1: + if skip: + skip = False + else: + self.device.screenshot() + + # End + if not self.appear(IN_RETIREMENT_CHECK): + break + + if self.appear_then_click(BACK_ARROW, offset=(20, 20)): + continue + + @property + def _retire_amount(self): + if self.config.RETIRE_MODE == 'all': + return 2000 + if self.config.RETIRE_MODE == '10': + return 10 + return 10 + + @property + def _retire_rarity(self): + rarity = set() + if self.config.RETIRE_N: + rarity.add('N') + if self.config.RETIRE_R: + rarity.add('R') + if self.config.RETIRE_SR: + rarity.add('SR') + if self.config.RETIRE_SSR: + rarity.add('SSR') + return rarity + + def retire_ships(self, amount=None, rarity=None): + """ + Args: + amount (int): Amount of cards retire. 0 to 2000. + rarity (tuple(str)): Card rarity. N, R, SR, SSR. + """ + if amount is None: + amount = self._retire_amount + if rarity is None: + rarity = self._retire_rarity + logger.hr('Retirement') + logger.info(f'Amount={amount}. Rarity={rarity}') + self._retirement_set_sort_method('ASC') + total = 0 + + while amount: + selected = self._retirement_choose(amount=10 if amount > 10 else amount, target_rarity=rarity) + total += selected + if selected == 0: + break + + self._retirement_confirm() + + amount -= selected + if amount <= 0: + break + + self._retirement_set_sort_method('DESC') + logger.info(f'Total retired: {total}') + + def handle_retirement(self, amount=None, rarity=None): + if not self.config.ENABLE_RETIREMENT: + return False + if not self.retirement_appear(): + return False + + self.ui_click(RETIRE_APPEAR_1, check_button=IN_RETIREMENT_CHECK, skip_first_screenshot=True) + self._handle_retirement_cards_loading() + + self.retire_ships(amount=amount, rarity=rarity) + + self._retirement_quit() diff --git a/module/ui/assets.py b/module/ui/assets.py new file mode 100644 index 000000000..120becdc7 --- /dev/null +++ b/module/ui/assets.py @@ -0,0 +1,19 @@ +from module.base.button import Button +from module.base.template import Template + +# This file is generated by module.dev_tools.asset_extract. +# Don't modified it manually. + +BACK_ARROW = Button(area=(36, 53, 82, 55), color=(251, 251, 255), button=(33, 31, 81, 78), file='./assets/ui/BACK_ARROW.png') +CAMPAIGN_GOTO_DAILY = Button(area=(804, 648, 892, 703), color=(189, 145, 78), button=(804, 648, 892, 703), file='./assets/ui/CAMPAIGN_GOTO_DAILY.png') +CAMPAIGN_GOTO_EVENT = Button(area=(804, 648, 892, 703), color=(189, 145, 78), button=(1178, 171, 1230, 223), file='./assets/ui/CAMPAIGN_GOTO_EVENT.png') +CAMPAIGN_GOTO_EXERCISE = Button(area=(1166, 648, 1248, 703), color=(177, 136, 69), button=(1166, 648, 1248, 703), file='./assets/ui/CAMPAIGN_GOTO_EXERCISE.png') +DAILY_CHECK = Button(area=(23, 656, 67, 698), color=(84, 139, 210), button=(23, 656, 67, 698), file='./assets/ui/DAILY_CHECK.png') +EVENT_CHECK = Button(area=(123, 63, 206, 109), color=(88, 104, 138), button=(123, 63, 206, 109), file='./assets/ui/EVENT_CHECK.png') +EXERCISE_CHECK = Button(area=(1065, 340, 1204, 382), color=(129, 166, 220), button=(1065, 340, 1204, 382), file='./assets/ui/EXERCISE_CHECK.png') +FLEET_CHECK = Button(area=(1044, 641, 1243, 702), color=(237, 186, 112), button=(1044, 641, 1243, 702), file='./assets/ui/FLEET_CHECK.png') +GOTO_MAIN = Button(area=(1230, 17, 1253, 45), color=(112, 132, 159), button=(1228, 18, 1255, 49), file='./assets/ui/GOTO_MAIN.png') +MAIN_GOTO_CAMPAIGN = Button(area=(1008, 289, 1154, 435), color=(216, 171, 99), button=(1008, 289, 1154, 435), file='./assets/ui/MAIN_GOTO_CAMPAIGN.png') +MAIN_GOTO_FLEET = Button(area=(733, 359, 987, 435), color=(110, 166, 234), button=(733, 359, 987, 435), file='./assets/ui/MAIN_GOTO_FLEET.png') +OCR_OIL_CV = Button(area=(634, 27, 714, 48), color=(93, 95, 109), button=(634, 27, 714, 48), file='./assets/ui/OCR_OIL_CV.png') +SP_CHECK = Button(area=(123, 63, 206, 109), color=(95, 110, 145), button=(123, 63, 206, 109), file='./assets/ui/SP_CHECK.png') diff --git a/module/ui/page.py b/module/ui/page.py new file mode 100644 index 000000000..2d069461a --- /dev/null +++ b/module/ui/page.py @@ -0,0 +1,62 @@ +import traceback + +from module.ui.assets import * + +MAIN_CHECK = MAIN_GOTO_CAMPAIGN +CAMPAIGN_CHECK = CAMPAIGN_GOTO_EXERCISE + + +class Page: + parent = None + + def __init__(self, check_button): + self.check_button = check_button + self.links = {} + (filename, line_number, function_name, text) = traceback.extract_stack()[-2] + self.name = text[:text.find('=')].strip() + + def __eq__(self, other): + return self.name == other.name + + def __hash__(self): + return hash(self.name) + + def __str__(self): + return self.name + + def link(self, button, destination): + self.links[destination] = button + + +# Main +page_main = Page(MAIN_CHECK) +page_campaign = Page(CAMPAIGN_CHECK) +page_fleet = Page(FLEET_CHECK) +page_main.link(button=MAIN_GOTO_CAMPAIGN, destination=page_campaign) +page_main.link(button=MAIN_GOTO_FLEET, destination=page_fleet) +page_campaign.link(button=GOTO_MAIN, destination=page_main) +page_fleet.link(button=GOTO_MAIN, destination=page_main) + +# Exercise +page_exercise = Page(EXERCISE_CHECK) +page_exercise.link(button=GOTO_MAIN, destination=page_main) +page_exercise.link(button=BACK_ARROW, destination=page_campaign) +page_campaign.link(button=CAMPAIGN_GOTO_EXERCISE, destination=page_exercise) + +# Daily +page_daily = Page(DAILY_CHECK) +page_daily.link(button=GOTO_MAIN, destination=page_main) +page_daily.link(button=BACK_ARROW, destination=page_campaign) +page_campaign.link(button=CAMPAIGN_GOTO_DAILY, destination=page_daily) + +# Event +page_event = Page(EVENT_CHECK) +page_event.link(button=GOTO_MAIN, destination=page_main) +page_event.link(button=BACK_ARROW, destination=page_campaign) +page_campaign.link(button=CAMPAIGN_GOTO_EVENT, destination=page_event) + +# SP +page_sp = Page(SP_CHECK) +page_sp.link(button=GOTO_MAIN, destination=page_main) +page_sp.link(button=BACK_ARROW, destination=page_campaign) +page_campaign.link(button=CAMPAIGN_GOTO_EVENT, destination=page_sp) diff --git a/module/ui/ui.py b/module/ui/ui.py new file mode 100644 index 000000000..1ef945c12 --- /dev/null +++ b/module/ui/ui.py @@ -0,0 +1,199 @@ +from module.base.base import ModuleBase +from module.base.button import Button +from module.base.ocr import Ocr +from module.base.timer import Timer +from module.logger import logger +from module.ui.page import * + + +class UI(ModuleBase): + ui_pages = [page_main, page_campaign, page_fleet, page_exercise, page_daily, page_event, page_sp] + ui_current: Page + + def ui_page_appear(self, page): + """ + Args: + page (Page): + """ + return self.appear(page.check_button, offset=(20, 20)) + + def ui_click(self, click_button, check_button, appear_button=None, offset=(20, 20), retry_wait=3, + skip_first_screenshot=False): + """ + Args: + click_button (Button): + check_button (Button, callable): + offset (bool, int, tuple): + retry_wait (int, float): + skip_first_screenshot (bool): + """ + logger.hr('UI click') + if appear_button is None: + appear_button = click_button + click_timer = Timer(retry_wait) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if isinstance(check_button, Button) and self.appear(check_button, offset=offset): + break + if callable(check_button) and check_button(): + break + + if click_timer.reached() and self.appear(appear_button, offset=offset): + self.device.click(click_button) + click_timer.reset() + continue + + def ui_get_current_page(self): + self.device.screenshot() + for page in self.ui_pages: + if self.ui_page_appear(page=page): + logger.attr('UI', page.name) + self.ui_current = page + return page + + logger.info('Unknown ui page') + if self.appear_then_click(GOTO_MAIN, offset=(20, 20)): + logger.info('Goto page_main') + self.wait_until_appear(MAIN_CHECK) + self.ui_current = page_main + return page_main + + logger.info('Unable to goto page_main') + + def ui_goto(self, destination): + """ + Args: + destination (Page): + """ + # Iter + visited = [self.ui_current] + visited = set(visited) + while 1: + new = visited.copy() + for page in visited: + for link in page.links.keys(): + if link in visited: + continue + link.parent = page + new.add(link) + if len(new) == len(visited): + break + visited = new + + # Find path + if destination.parent is None: + return [] + route = [destination] + while 1: + destination = destination.parent + if destination is not None: + route.append(destination) + else: + break + route.reverse() + if len(route) < 2: + logger.warning('No page route found.') + logger.attr('UI route', ' - '.join([p.name for p in route])) + + # Click + skip = False + for p1, p2 in zip(route[:-1], route[1:]): + # self.ui_click(source=p1, destination=p2) + self.ui_click(click_button=p1.links[p2], check_button=p2.check_button, offset=(20, 20), skip_first_screenshot=skip) + self.ui_current = p2 + skip = True + + # Reset + for page in visited: + page.parent = None + + def ui_ensure(self, destination): + """ + Args: + destination (Page): + """ + logger.hr('UI ensure') + self.ui_get_current_page() + if self.ui_current == destination: + logger.info('Already at %s' % destination) + return False + else: + logger.info('Goto %s' % destination) + self.ui_goto(destination) + return True + + def ui_goto_main(self): + return self.ui_ensure(destination=page_main) + + def handle_stage_icon_spawn(self): + self.device.sleep((1, 1.2)) + self.device.screenshot() + + def ui_weigh_anchor(self): + if self.ui_ensure(destination=page_campaign): + self.handle_stage_icon_spawn() + return True + else: + return False + + def ui_goto_event(self): + if self.ui_ensure(destination=page_event): + self.handle_stage_icon_spawn() + return True + else: + return False + + def ui_goto_sp(self): + if self.ui_ensure(destination=page_sp): + self.handle_stage_icon_spawn() + return True + else: + return False + + def ui_ensure_index(self, index, letter, next_button, prev_button, skip_first_screenshot=False, fast=True, + interval=(0.2, 0.3), step_sleep=(0.2, 0.3), finish_sleep=(0.5, 0.8)): + """ + Args: + index (int): + letter (Ocr, callable): OCR button. + next_button (Button): + prev_button (Button): + skip_first_screenshot (bool): + fast (bool): Default true. False when index is not continuous. + interval (tuple, int, float): Seconds between two click. + step_sleep (tuple, int, float): Seconds between two step. + finish_sleep (tuple, int, float): Second to wait when arrive. + """ + logger.hr('UI ensure index') + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if isinstance(letter, Ocr): + current = letter.ocr(self.device.image) + else: + current = letter(self.device.image) + + logger.attr('Index', current) + diff = index - current + if diff == 0: + break + + button = next_button if diff > 0 else prev_button + if fast: + self.device.multi_click(button, n=abs(diff), interval=interval) + else: + self.device.click(button) + self.device.sleep(step_sleep) + + self.device.sleep(finish_sleep) + + def ui_back(self, check_button, appear_button=None, offset=(20, 20), retry_wait=3, skip_first_screenshot=False): + return self.ui_click(click_button=BACK_ARROW, check_button=check_button, appear_button=appear_button, + offset=offset, retry_wait=retry_wait, skip_first_screenshot=skip_first_screenshot) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..c19116ce2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +numpy==1.14.6 +scipy +pillow +opencv-python +scikit-image +requests==2.18.4 +uiautomator2 +retrying +mxnet==1.4.1 +cnocr +gooey \ No newline at end of file diff --git a/template/ENEMY_L.png b/template/ENEMY_L.png new file mode 100644 index 000000000..05dd28e80 Binary files /dev/null and b/template/ENEMY_L.png differ diff --git a/template/ENEMY_M.png b/template/ENEMY_M.png new file mode 100644 index 000000000..229929833 Binary files /dev/null and b/template/ENEMY_M.png differ diff --git a/template/ENEMY_S.png b/template/ENEMY_S.png new file mode 100644 index 000000000..e0c6cff39 Binary files /dev/null and b/template/ENEMY_S.png differ