├── card ├── __init__.py ├── hero_power_card.py ├── id2card.py ├── basic_card.py ├── classic_card.py └── standard_card.py ├── constants ├── __init__.py └── constants.py ├── requirements.txt ├── .gitignore ├── demo ├── game_state_sanpshot_demo.py ├── get_window_name_demo.py └── mouse_control_demo.py ├── main.py ├── LICENSE ├── main.spec ├── json_op.py ├── print_info.py ├── catch_screen_demo.py ├── get_screen.py ├── README.md ├── log_op.py ├── click.py ├── strategy_entity.py ├── FSM_action.py ├── log_state.py └── strategy.py /card/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /constants/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keyboard 2 | opencv-python 3 | imagehash 4 | pynput 5 | pillow 6 | sklearn 7 | numpy 8 | pywin32 9 | requests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | venv/ 4 | img/ 5 | backup.md 6 | test* 7 | *.json 8 | *.log 9 | *.txt 10 | !requirements.txt 11 | 12 | dist/ 13 | build/ 14 | -------------------------------------------------------------------------------- /demo/game_state_sanpshot_demo.py: -------------------------------------------------------------------------------- 1 | from strategy import * 2 | 3 | if __name__ == "__main__": 4 | log_iter = log_iter_func(HEARTHSTONE_POWER_LOG_PATH) 5 | state = LogState() 6 | DEBUG_PRINT = 1 7 | 8 | log_container = next(log_iter) 9 | if log_container.log_type == LOG_CONTAINER_ERROR: 10 | sys_print("未找到Power.log,请启动炉石并开始对战") 11 | 12 | sys.exit(-1) 13 | 14 | for x in log_container.message_list: 15 | update_state(state, x) 16 | 17 | with open("game_state_snapshot.txt", "w", encoding="utf8") as f: 18 | f.write(str(state)) 19 | 20 | strategy_state = StrategyState(state) 21 | strategy_state.debug_print_out() 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from FSM_action import system_exit, AutoHS_automata 3 | import keyboard 4 | from log_state import check_name 5 | from print_info import print_info_init 6 | from FSM_action import init 7 | 8 | import constants.constants 9 | 10 | 11 | if __name__ == "__main__": 12 | print("请设置确认当前系统分辨率为:1920-1080") 13 | print("请设置确认当前系统缩放为:100%") 14 | print("请设置确认炉石传说设置为:全屏") 15 | 16 | MY_NAME = constants.constants.YOUR_NAME 17 | HEARTHSTONE_POWER_LOG_PATH = constants.constants.HEARTHSTONE_POWER_LOG_PATH 18 | #input("请确认你的用户名为:"+MY_NAME+"。炉石日志文件路径为:"+HEARTHSTONE_POWER_LOG_PATH+"(确认完成后请回车)") 19 | print("你好"+MY_NAME) 20 | sleep(2) 21 | # check_name() 22 | print_info_init() 23 | init() 24 | keyboard.add_hotkey("ctrl+q", system_exit) 25 | AutoHS_automata() 26 | -------------------------------------------------------------------------------- /demo/get_window_name_demo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import win32gui 4 | 5 | hwnd_title = {} 6 | NAME = "炉石传说" 7 | # NAME = "战网" 8 | 9 | 10 | def get_all_hwnd(hwnd, mouse): 11 | if win32gui.IsWindow(hwnd) and win32gui.IsWindowEnabled(hwnd) and win32gui.IsWindowVisible(hwnd): 12 | hwnd_title.update({hwnd: win32gui.GetWindowText(hwnd)}) 13 | 14 | 15 | if __name__ == "__main__": 16 | win32gui.EnumWindows(get_all_hwnd, 0) 17 | 18 | print(f"HWND : NAME") 19 | for h, t in hwnd_title.items(): 20 | if t is not "": 21 | print(h, ":", t) 22 | 23 | hwnd = win32gui.FindWindow(None, NAME) 24 | if hwnd == 0: 25 | print("未找到应用") 26 | sys.exit(-1) 27 | 28 | title = win32gui.GetWindowText(hwnd) 29 | clsname = win32gui.GetClassName(hwnd) 30 | print(title, ":", clsname) 31 | 32 | left, top, right, bottom = win32gui.GetWindowRect(hwnd) 33 | print("位置:", left, top, right, bottom) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or 8 | sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /main.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis(['main.py'], 8 | pathex=[], 9 | binaries=[], 10 | datas=[], 11 | hiddenimports=[], 12 | hookspath=[], 13 | hooksconfig={}, 14 | runtime_hooks=[], 15 | excludes=[], 16 | win_no_prefer_redirects=False, 17 | win_private_assemblies=False, 18 | cipher=block_cipher, 19 | noarchive=False) 20 | pyz = PYZ(a.pure, a.zipped_data, 21 | cipher=block_cipher) 22 | 23 | exe = EXE(pyz, 24 | a.scripts, 25 | [], 26 | exclude_binaries=True, 27 | name='main', 28 | debug=False, 29 | bootloader_ignore_signals=False, 30 | strip=False, 31 | upx=True, 32 | console=True, 33 | disable_windowed_traceback=False, 34 | target_arch=None, 35 | codesign_identity=None, 36 | entitlements_file=None ) 37 | coll = COLLECT(exe, 38 | a.binaries, 39 | a.zipfiles, 40 | a.datas, 41 | strip=False, 42 | upx=True, 43 | upx_exclude=[], 44 | name='main') 45 | -------------------------------------------------------------------------------- /demo/mouse_control_demo.py: -------------------------------------------------------------------------------- 1 | from pynput.mouse import Button, Controller 2 | import time 3 | 4 | MOUSE_SLEEP_INTERVAL = 3 # 3 seconds 5 | 6 | def main(): 7 | mouse = Controller() 8 | # Read pointer position 9 | print("The current pointer position is {0}".format(mouse.position)) 10 | print("Now please stare at your mouse, not the terminal :)") 11 | print("The mouse will move to the center of the screen\n") 12 | 13 | time.sleep(MOUSE_SLEEP_INTERVAL) 14 | # Set pointer position 15 | mouse.position = (1400, 900) 16 | print("Now we have moved it to {0}".format(mouse.position)) 17 | print("A move based on absolute position") 18 | print("Then the mouse will move to the right\n") 19 | 20 | time.sleep(MOUSE_SLEEP_INTERVAL) 21 | # Move pointer relative to current position 22 | mouse.move(50, 0) 23 | print("Now we have moved it to {0}".format(mouse.position)) 24 | print("A move based on relative position") 25 | print("Then we will click the right mouse button\n") 26 | 27 | time.sleep(MOUSE_SLEEP_INTERVAL) 28 | # Press and release 29 | mouse.press(Button.right) 30 | mouse.release(Button.right) 31 | 32 | print("Then we will move to the left and double click the left mouse button\n") 33 | time.sleep(MOUSE_SLEEP_INTERVAL) 34 | mouse.move(-50, 0) 35 | # Double click; this is different from pressing and releasing 36 | mouse.click(Button.left, 2) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() -------------------------------------------------------------------------------- /card/hero_power_card.py: -------------------------------------------------------------------------------- 1 | import click 2 | from card.basic_card import * 3 | 4 | 5 | class TotemicCall(HeroPowerCard): 6 | @classmethod 7 | def best_h_and_arg(cls, state, hand_card_index): 8 | if not state.my_hero_power.exhausted \ 9 | and state.my_minion_num < 7: 10 | return 0.1, 11 | else: 12 | return 0, 13 | 14 | @classmethod 15 | def use_with_arg(cls, state, card_index, *args): 16 | click.use_skill_no_point() 17 | time.sleep(1) 18 | 19 | 20 | class LesserHeal(HeroPowerCard): 21 | @classmethod 22 | def best_h_and_arg(cls, state, hand_card_index): 23 | if state.my_hero_power.exhausted: 24 | return 0, 25 | best_index = -1 26 | best_delta_h = state.my_hero.delta_h_after_heal(2) 27 | 28 | for my_index, my_minion in enumerate(state.my_minions): 29 | delta_h = my_minion.delta_h_after_heal(2) 30 | if delta_h > best_delta_h: 31 | best_delta_h = delta_h 32 | best_index = my_index 33 | 34 | return best_delta_h, best_index 35 | 36 | @classmethod 37 | def use_with_arg(cls, state, card_index, *args): 38 | click.use_skill_point_mine(args[0], state.my_minion_num) 39 | time.sleep(1) 40 | 41 | class BallistaShot(HeroPowerCard): 42 | @classmethod 43 | def best_h_and_arg(cls, state, hand_card_index): 44 | return 1,-1 45 | 46 | @classmethod 47 | def use_with_arg(cls, state, card_index, *args): 48 | click.use_skill_no_point() 49 | time.sleep(1) 50 | -------------------------------------------------------------------------------- /json_op.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import requests 4 | import json 5 | import os 6 | from print_info import * 7 | 8 | 9 | # 来源于互联网的炉石JSON数据下载API, 更多信息可以访问 https://hearthstonejson.com/ 10 | def download_json(json_path): 11 | json_url = "https://api.hearthstonejson.com/v1/latest/zhCN/cards.json" 12 | file = requests.get(json_url) 13 | 14 | with open(json_path, "wb") as f: 15 | f.write(file.content) 16 | 17 | 18 | def read_json(re_download=False): 19 | dir_path = os.path.dirname(__file__) 20 | if dir_path == "": 21 | dir_path = "." 22 | json_path = dir_path + "/cards.json" 23 | 24 | if not os.path.exists(json_path): 25 | sys_print("未找到cards.json,试图通过网络下载文件") 26 | download_json(json_path) 27 | elif re_download: 28 | sys_print("疑似有新版本炉石数据,正在重新下载最新文件") 29 | download_json(json_path) 30 | else: 31 | sys_print("cards.json已存在") 32 | 33 | with open(json_path, "r", encoding="utf8") as f: 34 | json_string = f.read() 35 | json_list = json.loads(json_string) 36 | json_dict = {} 37 | for item in json_list: 38 | json_dict[item["id"]] = item 39 | return json_dict 40 | 41 | 42 | def query_json_dict(key): 43 | global JSON_DICT 44 | 45 | if key == "": 46 | return "Unknown" 47 | 48 | if key in JSON_DICT: 49 | return JSON_DICT[key]["name"] 50 | # 认为是炉石更新了,出现了新卡,需要重新下载。 51 | else: 52 | JSON_DICT = read_json(True) 53 | if key not in JSON_DICT: 54 | error_print("出现未识别卡牌,程序无法继续") 55 | sys.exit(-1) 56 | return JSON_DICT[key]["name"] 57 | 58 | 59 | JSON_DICT = read_json() 60 | 61 | if __name__ == "__main__": 62 | with open("id-name.txt", "w", encoding="utf8") as f: 63 | for key, val in JSON_DICT.items(): 64 | f.write(key + " " + val["name"] + "\n") 65 | 66 | query_json_dict("SW_085t") 67 | -------------------------------------------------------------------------------- /card/id2card.py: -------------------------------------------------------------------------------- 1 | from card.basic_card import Coin 2 | from card.standard_card import * 3 | from card.classic_card import * 4 | from card.hero_power_card import * 5 | 6 | ID2CARD_DICT = { 7 | # 特殊项-幸运币 8 | "COIN": Coin, 9 | 10 | # 英雄技能 11 | "TOTEMIC_CALL": TotemicCall, 12 | "LESSER_HEAL": LesserHeal, 13 | "BALLISTA_SHOT": BallistaShot, 14 | 15 | # 标准模式-牧师 16 | "YOP_032": ArmorVendor, # 护甲商贩 17 | "CORE_CS1_130": HolySmite, # 神圣惩击 18 | "CS1_130": HolySmite, # 神圣惩击 19 | "SCH_250": WaveOfApathy, # 倦怠光波 20 | "BT_715": BonechewerBrawler, # 噬骨殴斗者 21 | "CORE_EX1_622": ShadowWordDeath, # 暗言术:灭 22 | "EX1_622": ShadowWordDeath, # 暗言术:灭 23 | "BT_257": Apotheosis, # 神圣化身 24 | "BAR_026": DeathsHeadCultist, # 亡首教徒 25 | "BAR_311": DevouringPlague, # 噬灵疫病 26 | "BT_730": OverconfidentOrc, # 狂傲的兽人 27 | "CORE_CS1_112": HolyNova, # 神圣新星 28 | "CS1_112": HolyNova, # 神圣新星 29 | "YOP_006": Hysteria, # 狂乱 30 | "CORE_EX1_197": ShadowWordRuin, # 暗言术:毁 31 | "EX1_197": ShadowWordRuin, # 暗言术:毁 32 | "WC_014": AgainstAllOdds, # 除奇致胜 33 | "BT_720": RuststeedRaider, # 锈骑劫匪 34 | "CS3_024": TaelanFordring, # 泰兰·弗丁 35 | "EX1_110": CairneBloodhoof, # 凯恩·血蹄 36 | "CORE_EX1_110": CairneBloodhoof, # 凯恩·血蹄 37 | "WC_030": MutanusTheDevourer, # 吞噬者穆坦努斯 38 | "BT_198": SoulMirror, # 灵魂之镜 39 | "DMF_053": BloodOfGhuun, # 戈霍恩之血 40 | 41 | # 经典模式 42 | "VAN_CS2_042": FireElemental, 43 | "VAN_EX1_562": Onyxia, 44 | "VAN_EX1_248": FeralSpirit, 45 | "VAN_EX1_246": Hex, 46 | "VAN_EX1_238": LightingBolt, 47 | "VAN_EX1_085": MindControlTech, 48 | "VAN_EX1_284": AzureDrake, 49 | "VAN_EX1_259": LightningStorm, 50 | "VAN_CS2_189": ElvenArcher, 51 | "VAN_CS2_117": EarthenRingFarseer, 52 | "VAN_EX1_097": Abomination, 53 | "VAN_NEW1_021": DoomSayer, 54 | "VAN_NEW1_041": StampedingKodo, 55 | "VAN_EX1_590": BloodKnight, 56 | "VAN_EX1_247": StormforgedAxe, 57 | } 58 | -------------------------------------------------------------------------------- /constants/constants.py: -------------------------------------------------------------------------------- 1 | # 你的Power.log的路径, 应该在你的炉石安装目录下的`Logs/`文件夹中, 这里放的是我的路径 2 | # ** 一定要修改成自己电脑上的路径 ** 3 | HEARTHSTONE_POWER_LOG_PATH = "C:/Program Files (x86)/Hearthstone/Logs/Power.log" 4 | 5 | # 你的炉石用户名, 注意英文标点符号'#', 把后面的数字也带上 6 | # 可以输入中文 7 | YOUR_NAME = "" 8 | 9 | # 关于控制台信息打印的设置 10 | DEBUG_PRINT = True 11 | WARN_PRINT = True 12 | SYS_PRINT = True 13 | INFO_PRINT = True 14 | ERROR_PRINT = True 15 | 16 | # 关于文件信息输出的设置 17 | DEBUG_FILE_WRITE = True 18 | WARN_FILE_WRITE = True 19 | SYS_FILE_WRITE = True 20 | INFO_FILE_WRITE = True 21 | ERROR_FILE_WRITE = True 22 | 23 | # 每个回合开始发个表情的概率 24 | EMOJ_RATIO = 0.15 25 | 26 | # 随从相互攻击的启发值包括两个部分:敌方随从受伤的带来的收益; 27 | # 以及我方随从受伤带来的损失。下面两个比例表示这两个启发值变化 28 | # 数值应该以怎样权值比例相加。如果是控制卡组,可以略微调高 29 | # OPPO_DELTA_H_FACTOR 来鼓励解场 30 | OPPO_DELTA_H_FACTOR = 1.2 31 | MY_DELTA_H_FACTOR = 1 32 | 33 | # 对于没有单独建一个类去描述的卡牌, 如果它的法力值花费大于这个值, 34 | # 就在流留牌阶段被换掉 35 | REPLACE_COST_BAR = 3 36 | 37 | OPERATE_INTERVAL = 0.2 38 | STATE_CHECK_INTERVAL = 1 39 | TINY_OPERATE_INTERVAL = 0.1 40 | BASIC_MINION_PUT_INTERVAL = 0.8 41 | BASIC_SPELL_WAIT_TIME = 1.5 42 | BASIC_WEAPON_WAIT_TIME = 1 43 | 44 | # 我觉得这行注释之后的内容应该不需要修改…… 45 | FSM_LEAVE_HS = "Leave Hearth Stone" 46 | FSM_MAIN_MENU = "Main Menu" 47 | FSM_CHOOSING_HERO = "Choosing Hero" 48 | FSM_MATCHING = "Match Opponent" 49 | FSM_CHOOSING_CARD = "Choosing Card" 50 | # FSM_NOT_MY_TURN = "Not My Turn" 51 | # FSM_MY_TURN = "My Turn" 52 | FSM_BATTLING = "Battling" 53 | FSM_ERROR = "ERROR" 54 | FSM_QUITTING_BATTLE = "Quitting Battle" 55 | FSM_WAIT_MAIN_MENU = "Wait main menu" 56 | 57 | LOG_CONTAINER_ERROR = 0 58 | LOG_CONTAINER_INFO = 1 59 | 60 | LOG_LINE_CREATE_GAME = "Create Game" 61 | LOG_LINE_GAME_ENTITY = "Create Game Entity" 62 | LOG_LINE_PLAYER_ENTITY = "Create Player Entity" 63 | LOG_LINE_FULL_ENTITY = "Full Entity" 64 | LOG_LINE_SHOW_ENTITY = "Show Entity" 65 | LOG_LINE_CHANGE_ENTITY = "Change Entity" 66 | LOG_LINE_BLOCK_START = "Block Start" 67 | LOG_LINE_BLOCK_END = "Block End" 68 | LOG_LINE_PLAYER_ID = "Player ID" 69 | LOG_LINE_TAG_CHANGE = "Tag Change" 70 | LOG_LINE_TAG = "Tag" 71 | 72 | CARD_BASE = "BASE" 73 | CARD_SPELL = "SPELL" 74 | CARD_MINION = "MINION" 75 | CARD_WEAPON = "WEAPON" 76 | CARD_HERO = "HERO" 77 | CARD_HERO_POWER = "HERO_POWER" 78 | CARD_ENCHANTMENT = "ENCHANTMENT" 79 | 80 | SPELL_NO_POINT = 0 81 | SPELL_POINT_OPPO = 1 82 | SPELL_POINT_MINE = 2 83 | -------------------------------------------------------------------------------- /print_info.py: -------------------------------------------------------------------------------- 1 | from constants.constants import * 2 | import os 3 | import time 4 | 5 | error_file_handle = None 6 | warn_file_handle = None 7 | debug_file_handle = None 8 | sys_file_handle = None 9 | info_file_handle = None 10 | 11 | 12 | def print_info_init(): 13 | global error_file_handle 14 | global warn_file_handle 15 | global debug_file_handle 16 | global sys_file_handle 17 | global info_file_handle 18 | 19 | if not os.path.exists("./log/"): 20 | os.mkdir("./log/") 21 | 22 | error_file_handle = open("./log/error_log.txt", "w", encoding="utf8") 23 | warn_file_handle = open("./log/warn_log.txt", "w", encoding="utf8") 24 | debug_file_handle = open("./log/debug_log.txt", "w", encoding="utf8") 25 | sys_file_handle = open("./log/sys_log.txt", "w", encoding="utf8") 26 | info_file_handle = open("./log/info_log.txt", "w", encoding="utf8") 27 | 28 | 29 | def print_info_close(): 30 | global error_file_handle 31 | global warn_file_handle 32 | global debug_file_handle 33 | global sys_file_handle 34 | global info_file_handle 35 | 36 | error_file_handle.close() 37 | error_file_handle = None 38 | warn_file_handle.close() 39 | warn_file_handle = None 40 | debug_file_handle.close() 41 | debug_file_handle = None 42 | sys_file_handle.close() 43 | sys_file_handle = None 44 | info_file_handle.close() 45 | info_file_handle = None 46 | 47 | def current_time(): 48 | return time.strftime("%H:%M:%S", time.localtime()) 49 | 50 | def error_print(error_str): 51 | error_str = f"[{current_time()} ERROR] {error_str}" 52 | 53 | if ERROR_PRINT: 54 | print(error_str) 55 | if ERROR_FILE_WRITE and error_file_handle: 56 | error_file_handle.write(error_str + "\n") 57 | 58 | 59 | def warn_print(warn_str): 60 | warn_str = f"[{current_time()} WARN] {warn_str}" 61 | 62 | if WARN_PRINT: 63 | print(warn_str) 64 | if WARN_FILE_WRITE and warn_file_handle: 65 | warn_file_handle.write(warn_str+ "\n") 66 | 67 | 68 | def debug_print(debug_str=""): 69 | debug_str = f"[{current_time()} DEBUG] {debug_str}" 70 | 71 | if DEBUG_PRINT: 72 | print(debug_str) 73 | if DEBUG_FILE_WRITE and debug_file_handle: 74 | debug_file_handle.write(debug_str + "\n") 75 | 76 | 77 | def sys_print(sys_str): 78 | sys_str = f"[{current_time()} SYS] {sys_str}" 79 | 80 | if SYS_PRINT: 81 | print(sys_str) 82 | if SYS_FILE_WRITE and sys_file_handle: 83 | sys_file_handle.write(sys_str + "\n") 84 | 85 | 86 | def info_print(info_str): 87 | info_str = f"[{current_time()} INFO] {info_str}" 88 | 89 | if INFO_PRINT: 90 | print(info_str) 91 | if INFO_FILE_WRITE and info_file_handle: 92 | info_file_handle.write(info_str + "\n") 93 | -------------------------------------------------------------------------------- /catch_screen_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | 主体代码引自 Demon_Hunter 的CSDN博客, 博客URL:https://blog.csdn.net/zhuisui_woxin/article/details/84345036 3 | """ 4 | import sys 5 | 6 | import cv2 7 | import time 8 | import math 9 | 10 | import get_screen 11 | 12 | 13 | PRINT_ALL_AREA_LIST = [] 14 | # AREA_LIST = [((1495, 465), (1620, 522))] 15 | AREA_LIST = [((690, 290), (710, 310))] 16 | 17 | POINT_LIST = [(960, 650), (1090, 1070), (705, 305)] 18 | 19 | 20 | def get_sum(x): 21 | return int(x[0]) + int(x[1]) + int(x[2]) 22 | 23 | 24 | def add_line(img, width, height): 25 | for i in range(1, math.floor(width / 100) + 1): 26 | cv2.line(img, pt1=(i * 100, 0), pt2=(i * 100, height), color=(200, 200, 200), thickness=1) 27 | cv2.putText(img, str(i * 100), (i * 100 - 30, 30), cv2.FONT_HERSHEY_COMPLEX, 1, (0, 0, 255), 1) 28 | 29 | for i in range(1, math.floor(height / 100) + 1): 30 | cv2.line(img, pt1=(0, i * 100), pt2=(width, i * 100), color=(200, 200, 200), thickness=1) 31 | cv2.putText(img, str(i * 100), (0, i * 100), cv2.FONT_HERSHEY_COMPLEX, 1, (0, 0, 255), 1) 32 | 33 | return img 34 | 35 | 36 | def add_point(img, point_list): 37 | for pair in point_list: 38 | print(str(pair) + " has color: " + str(img[pair[1]][pair[0]])) 39 | cv2.circle(img, pair, 1, (255, 0, 0), 2, 0) 40 | 41 | 42 | def show_area(img, top_left, bottom_right, print_out=False): 43 | x1, y1 = top_left 44 | x2, y2 = bottom_right 45 | tmp_img = img[y1:y2, x1:x2] 46 | tmp_img = tmp_img.copy() 47 | 48 | if print_out: 49 | count = 0 50 | for line in tmp_img: 51 | for pixel in line: 52 | if pixel[1] > 230: 53 | count += 1 54 | print(count) 55 | 56 | ratio = min(round(800 / (x2 - x1)), round(500 / (y2 - y1))) 57 | resized_x_length = (x2 - x1) * int(ratio) 58 | resized_y_length = int(resized_x_length * ((y2 - y1) / (x2 - x1))) 59 | tmp_img = cv2.resize(tmp_img, (resized_x_length, resized_y_length)) 60 | 61 | font_size = int(ratio) / 8 62 | for x in range(x1, x2, 10): 63 | temp_x = int(resized_x_length / (x2 - x1) * (x - x1)) 64 | cv2.line(tmp_img, pt1=(temp_x, 0), pt2=(temp_x, resized_y_length), color=(200, 200, 200), thickness=1) 65 | cv2.putText(tmp_img, str(x), (temp_x, 10), cv2.FONT_HERSHEY_COMPLEX, font_size, (0, 0, 255), 1) 66 | 67 | for y in range(y1, y2, 10): 68 | temp_y = int(resized_y_length * (y - y1) / (y2 - y1)) 69 | cv2.line(tmp_img, pt1=(0, temp_y), pt2=(resized_x_length, temp_y), color=(200, 200, 200), thickness=1) 70 | cv2.putText(tmp_img, str(y), (0, temp_y + 5), cv2.FONT_HERSHEY_COMPLEX, font_size, (0, 255, 0), 1) 71 | 72 | cv2.imshow("area", tmp_img) 73 | cv2.waitKey(0) 74 | cv2.destroyWindow("area") 75 | 76 | 77 | if __name__ == "__main__": 78 | im_opencv = get_screen.catch_screen() 79 | if im_opencv is None: 80 | print("未找到应用") 81 | sys.exit(-1) 82 | 83 | add_line(im_opencv, 1920, 1080) 84 | add_point(im_opencv, POINT_LIST) 85 | 86 | cv2.imshow("total", im_opencv) # 显示 87 | cv2.waitKey(0) 88 | cv2.destroyWindow("total") 89 | time.sleep(0.2) 90 | 91 | for area in AREA_LIST: 92 | if len(area) < 2 or len(area) > 3: 93 | print("[Usage]: ((left-top), (botton-right), [mouse-position])") 94 | top_left, bottom_right = area 95 | show_area(im_opencv, top_left, bottom_right) 96 | 97 | for area in PRINT_ALL_AREA_LIST: 98 | if len(area) < 2 or len(area) > 3: 99 | print("[Usage]: ((left-top), (botton-right), [mouse-position])") 100 | top_left, bottom_right = area 101 | show_area(im_opencv, top_left, bottom_right, print_out=True) 102 | 103 | cv2.destroyAllWindows() 104 | -------------------------------------------------------------------------------- /get_screen.py: -------------------------------------------------------------------------------- 1 | """ 2 | 主体代码引自 Demon_Hunter 的CSDN博客, 博客URL:https://blog.csdn.net/zhuisui_woxin/article/details/84345036 3 | """ 4 | 5 | import win32gui 6 | import win32ui 7 | import win32con 8 | import win32com.client 9 | import win32api 10 | import win32process 11 | import numpy 12 | from print_info import * 13 | 14 | from constants.constants import * 15 | 16 | 17 | def get_HS_hwnd(): 18 | hwnd = win32gui.FindWindow(None, "炉石传说") 19 | if hwnd != 0: 20 | return hwnd 21 | 22 | hwnd = win32gui.FindWindow(None, "《爐石戰記》") 23 | if hwnd != 0: 24 | return hwnd 25 | 26 | hwnd = win32gui.FindWindow(None, "Hearthstone") 27 | return hwnd 28 | 29 | 30 | def get_battlenet_hwnd(): 31 | hwnd = win32gui.FindWindow(None, "战网") 32 | if hwnd != 0: 33 | return hwnd 34 | 35 | hwnd = win32gui.FindWindow(None, "Battle.net") 36 | return hwnd 37 | 38 | 39 | def test_hs_available(): 40 | return get_HS_hwnd() != 0 41 | 42 | 43 | def move_window_foreground(hwnd, name=""): 44 | try: 45 | win32gui.BringWindowToTop(hwnd) 46 | shell = win32com.client.Dispatch("WScript.Shell") 47 | shell.SendKeys('%') 48 | win32gui.SetForegroundWindow(hwnd) 49 | except Exception as e: 50 | if name != "": 51 | warn_print(f"Open {name}: {e}") 52 | else: 53 | warn_print(e) 54 | 55 | win32gui.ShowWindow(hwnd, win32con.SW_NORMAL) 56 | 57 | 58 | def max_diff(img, pixel_list): 59 | ans = 0 60 | for pair in pixel_list: 61 | diff = abs(int(img[pair[0]][pair[1]][1]) - 62 | int(img[pair[0]][pair[1]][0])) 63 | ans = max(ans, diff) 64 | # print(img[pair[0]][pair[1]]) 65 | 66 | return ans 67 | 68 | 69 | def catch_screen(name=None): 70 | # 第一个参数是类名,第二个参数是窗口名字 71 | # hwnd -> Handle to a Window ! 72 | # 如果找不到对应名字的窗口,返回0 73 | if name is not None: 74 | hwnd = win32gui.FindWindow(None, name) 75 | else: 76 | hwnd = get_HS_hwnd() 77 | 78 | if hwnd == 0: 79 | return 80 | 81 | width = 1960 82 | height = 1080 83 | # 返回句柄窗口的设备环境,覆盖整个窗口,包括非客户区,标题栏,菜单,边框 DC device context 84 | hwin = win32gui.GetDesktopWindow() 85 | 86 | hwndDC = win32gui.GetWindowDC(hwin) 87 | # 创建设备描述表 88 | mfcDC = win32ui.CreateDCFromHandle(hwndDC) 89 | # 创建内存设备描述表 90 | saveDC = mfcDC.CreateCompatibleDC() 91 | # 创建位图对象准备保存图片 92 | saveBitMap = win32ui.CreateBitmap() 93 | # 为bitmap开辟存储空间 94 | saveBitMap.CreateCompatibleBitmap(mfcDC, width, height) 95 | # 将截图保存到saveBitMap中 96 | saveDC.SelectObject(saveBitMap) 97 | # 保存bitmap到内存设备描述表 98 | saveDC.BitBlt((0, 0), (width, height), mfcDC, (0, 0), win32con.SRCCOPY) 99 | 100 | signedIntsArray = saveBitMap.GetBitmapBits(True) 101 | 102 | # 内存释放 103 | win32gui.DeleteObject(saveBitMap.GetHandle()) 104 | saveDC.DeleteDC() 105 | mfcDC.DeleteDC() 106 | win32gui.ReleaseDC(hwnd, hwndDC) 107 | 108 | im_opencv = numpy.frombuffer(signedIntsArray, dtype='uint8') 109 | im_opencv.shape = (height, width, 4) 110 | 111 | return im_opencv 112 | 113 | 114 | def get_state(): 115 | hwnd = get_HS_hwnd() 116 | if hwnd == 0: 117 | return FSM_LEAVE_HS 118 | 119 | im_opencv = catch_screen() 120 | 121 | # print("当前定位点------------") 122 | # print(im_opencv[1070][1090][:3]) 123 | # 先y轴再z轴 124 | if list(im_opencv[1070][1090][:3]) == [23, 52, 105] or \ 125 | list(im_opencv[305][705][:3]) == [21, 43, 95] or \ 126 | list(im_opencv[1070][1090][:3]) == [20, 51, 104]: # 万圣节主界面会变 127 | return FSM_MAIN_MENU 128 | if list(im_opencv[1070][1090][:3]) == [8, 18, 24]: 129 | return FSM_CHOOSING_HERO 130 | if list(im_opencv[1070][1090][:3]) == [17, 18, 19]: 131 | return FSM_MATCHING 132 | if list(im_opencv[860][960][:3]) == [71, 71, 71]: 133 | return FSM_CHOOSING_CARD 134 | 135 | return FSM_BATTLING 136 | 137 | 138 | # def image_hash(img): 139 | # img = Image.fromarray(img) 140 | # return imagehash.phash(img) 141 | # 142 | # 143 | # def hash_diff(str1, str2): 144 | # return bin(int(str1, 16) ^ int(str2, 16))[2:].count("1") 145 | 146 | 147 | def terminate_HS(): 148 | hwnd = get_HS_hwnd() 149 | if hwnd == 0: 150 | return 151 | _, process_id = win32process.GetWindowThreadProcessId(hwnd) 152 | handle = win32api.OpenProcess(win32con.PROCESS_TERMINATE, 0, process_id) 153 | win32api.TerminateProcess(handle, 0) 154 | win32api.CloseHandle(handle) 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoHS(欢迎star) 2 | * 动机:网上的炉石传说脚本都是付费的,像我这样的白嫖党很难受,所以希望有一个免费的脚本,哪怕简陋,但晒可以跑,做做任务刷刷金币即可 3 | * 致谢:本代码参考[Yiyuan-Dong](https://github.com/Yiyuan-Dong/AutoHS)和[FallAbyss](https://github.com/FallAbyss/AutoHS),修改了一些小bug,制作了一个很简易的经典模式脚本 4 | * 注意:这个脚本是pc版基于页面的脚本,也就是完全模拟你手动的点击,需要一台win机器,并且装有炉石 5 | 6 | ### 如何运行 7 | 8 | 0. 安装`Python3`。 9 | 10 | 1. 安装所需依赖: 11 | ``` 12 | pip install -r requirements.txt 13 | ``` 14 | 15 | 2. 在`constants/constants`里有一些参数可以设置,其中有两项必须修改: 16 | - 名为`YOUR_NAME`的变量需要改成你的炉石用户名,形如`为所欲为、异灵术#54321`。如果不在文件里修改,每次启动脚本时系统都会提示你手动输入用户名。 17 | - 名为`HEARTHSTONE_POWER_LOG_PATH`的变量必须修改成你的电脑上的炉石传说日志`Power.log`的路径,`Power.log`在炉石安装路径下的`Logs/`文件夹中。 18 | 19 | > `Power.log`中记录了对战过程中每一个**对象**(**Entity**)的每一项**属性**(**tag**)的变化。 这个**对象**包括玩家、英雄、英雄技能、卡牌(无论在牌库里、手牌中、战场上还是坟地里)等。 20 | > 21 | > `Power.log`会在进入炉石后第一次对战开始时创建,在退出炉石后会被重命名为`Power_bk.log`,在再一次进入炉石时被删除。 22 | > 23 | > 如果你在`Logs/`目录下没有找到`Power.log`(指对战开始后),那稍微有一些麻烦。你需要到`C:\Users\YOURUSER\AppData\Local\Blizzard\Hearthstone`目录下新建一个叫`log.config`的文件(如果已经有就不用新建了),然后把下面这段代码放进去(如果已经有`[Power]`相关则更改相关设置): 24 | > ``` 25 | > [Power] 26 | > LogLevel=1 27 | > FilePrinting=True 28 | > ConsolePrinting=False 29 | > ScreenPrinting=False 30 | > Verbose=True 31 | > ``` 32 | > 33 | > 关于炉石log的更多信息可以查看这个 34 | > [Reddit帖子](https://www.reddit.com/r/hearthstone/comments/268fkk/simple_hearthstone_logging_see_your_complete_play/) 。 35 | 36 | 3. 可以先跑一跑`demo/`下的一些文件。 37 | 38 | 4. 若要启动脚本,将当前目录切换到`AutoHS/`下(重要),运行`python main.py`即可。注意以下几点: 39 | - **显示分辨率**(在桌面右击的显示设置里调整)以及**炉石分辨率**为**1920 * 1080**。 40 | - 项目大小缩放比例为**100%**(同样在显示设置里调整)。 41 | - 炉石**全屏**且语言为**简体中文**、**繁体中文**或**英文**。 42 | - 炉石放在**最前台**。 43 | - 你可以把战网客户端最小化到任务栏,或是放在炉石应用下面,但请不要关闭战网客户端。有时炉石会意外关闭,这时程序会试图重新打开炉石。 44 | 45 | 46 | ### 我目前用的挂机卡组 47 | #### 经典模式-动物园的亲爹 48 | - 2x (1) 精灵弓箭手 49 | - 2x (1) 银色侍从 50 | - 2x (2) 战利品贮藏者 51 | - 2x (2) 末日预言者 52 | - 1x (2) 血法师萨尔诺斯 53 | - 2x (2) 雷铸战斧 54 | - 2x (3) 大地之环先知 55 | - 1x (3) 妖术 56 | - 2x (3) 精神控制技师 57 | - 2x (3) 苦痛侍僧 58 | - 1x (3) 血骑士 59 | - 2x (3) 闪电风暴 60 | - 1x (4) 冰风雪人 61 | - 2x (4) 森金持盾卫士 62 | - 2x (5) 土元素 63 | - 2x (5) 狂奔科多兽 64 | - 2x (6) 烈日行者 65 | 66 | 神秘代码: 67 | 68 | ``` 69 | AAEDAfWfAwSwlgTnlgS1oQSWowQNr5YEs5YE+qAEsaEEsqEEvqEE0KEE1aEEiKIEi6IEjqIExaME0qMEAA== 70 | ``` 71 | 72 | 73 | 在广大动物园脚本的鼎力支持下,原作者Yiyuan-Dong已于2021年8月14日上传说。上传说时排名6785,上传说前29场对战战绩为20-9。 74 | 75 | 没必要特意去合卡,所有卡牌都可以替换成任意**非战吼随从**,或是`card/classic_card.py`中已经写过逻辑的卡牌。比如卡组中血法师萨尔诺斯这张牌据我观察毫无作用,只是鉴于第一次上传说的纪念意义予以保留。 76 | 77 | ### 如果想加入新的卡牌 78 | 对于所有的**非战吼随从**,如果没有具体实现它,脚本会根据它的费用猜测它的价值(费用越高越厉害)。而脚本不会使用未识别的战吼随从、法术、武器。如果想要让脚本识别并合理运用一张卡牌,你需要干两件事: 79 | 1. 在`card/classic_card.py`中写下它的使用规则,比如使用它的期望值、使用它时鼠标要点哪里等。 80 | 2. 在`card/id2card.py`中加入一个它的**id**与其卡牌实现类的键值对。 81 | 82 | > 关于**id**,在炉石中每一个**Entity**都有其对应的**id**,比如各种各样的吉安娜有各种各样的**id**: 83 | > - HERO_08 吉安娜·普罗德摩尔 84 | > - HERO_08c 火法师吉安娜 85 | > - HERO_08f 学生吉安娜 86 | > - HERO_08g 奥术师吉安娜 87 | > - HERO_08h 学徒吉安娜 88 | > - HERO_08i 大法师吉安娜 89 | > - HERO_08j 库尔提拉斯的吉安娜 90 | > - HERO_08k 灵风吉安娜 91 | > 92 | > 英雄技能各有**id**,一个同样的技能会有好多**id**,并随着皮肤的切换改变**id**: 93 | > - HERO_07bp 生命分流 94 | > - HERO_07dbp 生命分流 95 | > - HERO_07ebp 生命分流 96 | > - VAN_HERO_07bp 生命分流 97 | > - CS2_056_H1 生命分流 98 | > - CS2_056_H2 生命分流 99 | > - CS2_056_H3 生命分流 100 | > - HERO_07bp2 灵魂分流(生命分流的升级技能) 101 | > 102 | > 卡牌,连带着它的衍生牌、增益效果、抉择选项,各有各的**id**: 103 | > - SW_091 恶魔之种 104 | > - SW_091t 建立连接 105 | > - SW_091t3 完成仪式 106 | > - SW_091t4 枯萎化身塔姆辛 107 | > - SW_091t5 枯萎化身 108 | > 109 | > 如果想获取卡牌的**id**,可以直接运行`json_op.py`,它会在脚本根目录下生成一个名为`id-name.txt`的文件,包含了炉石中每一个对象的**id**与中文名的对应关系。 110 | 111 | ### 文件说明 112 | - `demo/catch_screen_demo.py`: 运行此文件会获取炉石传说进程的整个截屏(无论是在前台还是后台),并画上一些坐标基准线,方便判断想实现的操作的坐标值。 113 | - `demo/game_state_snapshot_demo.py`: 在控制台显示目前的炉石战局情况,包括显示手牌情况,英雄情况,随从情况等; 还会在`demo/`目录下创建一个名为`game_state_sanpshot.txt`的文件,记录log分析情况。 需要在`Power.log`存在,即进入对战模式后调用。 114 | - `demo/get_window_name.py`: 显示当前所有窗口的名称和编号,可以用来看炉石传说叫什么名字…… 115 | - `demo/mouse_control_demo.py`: 一个样例程序,展示了如何控制鼠标。 116 | - `click.py`: 包含了与鼠标控制相关的代码。 117 | - `FSM_action.py`: 包含了脚本在炉石运行中的不同状态(比如选英雄界面、对战时、对扎结束后)应该采取什么行为以及何时进入下一站状态的代码。 118 | - `lop_op.py`: 包含了与读取`Power.log`相关的代码,比如针对不同日志行的正则表达式。 119 | - `json_op.py`: 包含了从网络上下载炉石数据JSON文件,并将其初步处理的代码。直接运行可以生成`id-name.txt`,一个包含了炉石所有对象**id**与中文名对应关系的文件。 120 | - `log_state.py`: 读取`log.py`提取的日志信息,并把他们转化成字典的列表的形式。每一个字典是一个 **Entity**,**Entity** 由不同的 **tag** 及其对应值构成。 121 | - `strategy.py`: 读取`log_state.py`提取的信息,并从中提取出手牌信息,战场信息,墓地信息等,再根据这些具体信息思考行动策略。 122 | - `card/`: 用于存放针对某些特殊卡牌的具体逻辑。 123 | 124 | 125 | 126 | [comment]: <> (### 关于控制鼠标) 127 | 128 | [comment]: <> (原本想通过发送信号的方式在让炉石在后台也能接收到鼠标点击) 129 | 130 | [comment]: <> (但是发现炉石应该是所谓的接受直接输入的进程,信号模拟它不会接收……) 131 | 132 | [comment]: <> (所以只能使用很low的鼠标点击了) 133 | 134 | [comment]: <> (也许能直接模拟网络发包?) 135 | 136 | 137 | [comment]: <> (### 关于网络连接的观察) 138 | 139 | [comment]: <> (一打开炉石就会建立两个TCP连接,这两个所有的数据都是加密的。像分解卡牌, 只有退出了某个卡牌的分解界面(就是可以撤销的界面)才会发包确认分解结果。) 140 | 141 | [comment]: <> (实验下来感觉只有其中一条连接在真的交换数据。) 142 | 143 | [comment]: <> (点击匹配会新建一个连接,这个连接是加密的。在匹配完成后连接就销毁。) 144 | 145 | [comment]: <> (进入对战会又新建一个连接,这个是纯TCP没有加密,不过我仍然无法解析数据交换的格式……。) 146 | 147 | [comment]: <> (任何一个操作都会触发数据传输(比如空中乱晃鼠标……),而如果什么都不做炉石也会每个5秒跟服务器互相ping一下,应该是在确认是否掉线) 148 | 149 | 150 | 151 | ### 补充 152 | 153 | * 如果怕被封号,建议每天只玩几个小时,别大半夜的还在刷脚本,这部分在`FSM_action.py/def AutoHS_automata()`中有设置,我基本是每天玩到晚上11点就停了,因为实验室是9点下班,所以基本在下班前开启脚本,9-11点刷脚本,各位大佬不要跟我撞时间了(滑稽) 154 | * 另外我还在原来的基础上设置了一些随机数,防止每个操作都间隔一样的时间,被系统发现脚本 155 | * 新的策略可以在阅读源码的基础上做出自己的修改,各位大佬加油,可以给我提pr -------------------------------------------------------------------------------- /card/basic_card.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | from abc import ABC, abstractmethod 4 | import click 5 | from constants.constants import * 6 | from print_info import * 7 | 8 | 9 | class Card(ABC): 10 | # 用来指示是否在留牌阶段把它留下, 默认留下 11 | # 在 keep_in_hand 中返回 12 | keep_in_hand_bool = True 13 | 14 | @classmethod 15 | def keep_in_hand(cls, state, hand_card_index): 16 | return cls.keep_in_hand_bool 17 | 18 | # 用来指示这张卡的价值, 在 best_h_and_arg 中返回. 19 | # 如果为 0 则代表未设置, 会根据卡牌费用等信息区估算价值. 20 | # 一些功能卡不能用一个简单的数值去评判价值, 应针对其另写 21 | # 函数 22 | value = 0 23 | 24 | # 返回两个东西,第一项是使用这张卡的\delta h, 25 | # 之后是是用这张卡的最佳参数,参数数目不定 26 | # 参数是什么呢,比如一张火球术,参数就是指示你 27 | # 是要打脸还是打怪 28 | @classmethod 29 | def best_h_and_arg(cls, state, hand_card_index): 30 | return cls.value, 31 | 32 | @classmethod 33 | @abstractmethod 34 | def use_with_arg(cls, state, card_index, *args): 35 | pass 36 | 37 | @classmethod 38 | @abstractmethod 39 | def get_card_type(cls): 40 | pass 41 | 42 | 43 | class SpellCard(Card): 44 | wait_time = BASIC_SPELL_WAIT_TIME 45 | 46 | @classmethod 47 | def get_card_type(cls): 48 | return CARD_SPELL 49 | 50 | 51 | class SpellNoPoint(SpellCard): 52 | @classmethod 53 | def use_with_arg(cls, state, card_index, *args): 54 | click.choose_and_use_spell(card_index, state.my_hand_card_num) 55 | click.cancel_click() 56 | time.sleep(cls.wait_time) 57 | 58 | 59 | class SpellPointOppo(SpellCard): 60 | @classmethod 61 | def use_with_arg(cls, state, card_index, *args): 62 | if len(args) == 0: 63 | hand_card = state.my_hand_cards[card_index] 64 | warn_print(f"Receive 0 args in using SpellPointOppo card {hand_card.name}") 65 | return 66 | 67 | oppo_index = args[0] 68 | click.choose_card(card_index, state.my_hand_card_num) 69 | if oppo_index >= 0: 70 | click.choose_opponent_minion(oppo_index, state.oppo_minion_num) 71 | else: 72 | click.choose_oppo_hero() 73 | click.cancel_click() 74 | time.sleep(cls.wait_time) 75 | 76 | 77 | class SpellPointMine(SpellCard): 78 | @classmethod 79 | def use_with_arg(cls, state, card_index, *args): 80 | if len(args) == 0: 81 | hand_card = state.my_hand_cards[card_index] 82 | warn_print(f"Receive 0 args in using SpellPointMine card {hand_card.name}") 83 | return 84 | 85 | mine_index = args[0] 86 | click.choose_card(card_index, state.my_hand_card_num) 87 | click.choose_my_minion(mine_index, state.my_minion_num) 88 | click.cancel_click() 89 | time.sleep(cls.wait_time) 90 | 91 | 92 | class MinionCard(Card): 93 | @classmethod 94 | def get_card_type(cls): 95 | return CARD_MINION 96 | 97 | @classmethod 98 | def basic_delta_h(cls, state, hand_card_index): 99 | if state.my_minion_num >= 7: 100 | return -20000 101 | else: 102 | return 0 103 | 104 | @classmethod 105 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 106 | if cls.value != 0: 107 | return cls.value, state.my_minion_num 108 | else: 109 | # 费用越高的应该越厉害吧 110 | hand_card = state.my_hand_cards[hand_card_index] 111 | delta_h = hand_card.current_cost / 2 + 1 112 | 113 | if state.my_hero.health <= 10 and hand_card.taunt: 114 | delta_h *= 1.5 115 | 116 | return delta_h, state.my_minion_num # 默认放到最右边 117 | 118 | @classmethod 119 | def combo_delta_h(cls, state, hand_card_index): 120 | h_sum = 0 121 | 122 | for my_minion in state.my_minions: 123 | # 有末日就别下怪了 124 | if my_minion.card_id == "VAN_NEW1_021": 125 | h_sum += -1000 126 | 127 | # 有飞刀可以多下怪 128 | if my_minion.card_id == "VAN_NEW1_019": 129 | h_sum += 0.5 130 | 131 | return h_sum 132 | 133 | @classmethod 134 | def best_h_and_arg(cls, state, hand_card_index): 135 | delta_h, *args = cls.utilize_delta_h_and_arg(state, hand_card_index) 136 | if len(args) == 0: 137 | args = [state.my_minion_num] 138 | 139 | delta_h += cls.basic_delta_h(state, hand_card_index) 140 | delta_h += cls.combo_delta_h(state, hand_card_index) 141 | return (delta_h,) + tuple(args) 142 | 143 | 144 | class MinionNoPoint(MinionCard): 145 | @classmethod 146 | def use_with_arg(cls, state, card_index, *args): 147 | gap_index = args[0] 148 | click.choose_card(card_index, state.my_hand_card_num) 149 | click.put_minion(gap_index, state.my_minion_num) 150 | click.cancel_click() 151 | time.sleep(BASIC_MINION_PUT_INTERVAL) 152 | 153 | 154 | class MinionPointOppo(MinionCard): 155 | @classmethod 156 | def use_with_arg(cls, state, card_index, *args): 157 | gap_index = args[0] 158 | oppo_index = args[1] 159 | 160 | click.choose_card(card_index, state.my_hand_card_num) 161 | click.put_minion(gap_index, state.my_minion_num) 162 | if oppo_index >= 0: 163 | click.choose_opponent_minion(oppo_index, state.oppo_minion_num) 164 | else: 165 | click.choose_oppo_hero() 166 | click.cancel_click() 167 | time.sleep(BASIC_MINION_PUT_INTERVAL) 168 | 169 | 170 | class MinionPointMine(MinionCard): 171 | @classmethod 172 | def use_with_arg(cls, state, card_index, *args): 173 | gap_index = args[0] 174 | my_index = args[1] 175 | 176 | click.choose_card(card_index, state.my_hand_card_num) 177 | click.put_minion(gap_index, state.my_minion_num) 178 | if my_index >= 0: 179 | # 这时这个随从已经在场上了, 其他随从已经移位了 180 | click.choose_my_minion(my_index, state.my_minion_num + 1) 181 | else: 182 | click.choose_my_hero() 183 | click.cancel_click() 184 | time.sleep(BASIC_MINION_PUT_INTERVAL) 185 | 186 | 187 | class WeaponCard(Card): 188 | @classmethod 189 | def get_card_type(cls): 190 | return CARD_WEAPON 191 | 192 | @classmethod 193 | def use_with_arg(cls, state, card_index, *args): 194 | click.choose_and_use_spell(card_index, state.my_hand_card_num) 195 | click.cancel_click() 196 | time.sleep(BASIC_WEAPON_WAIT_TIME) 197 | 198 | @classmethod 199 | def best_h_and_arg(cls, state, hand_card_index): 200 | if state.my_weapon: 201 | return 0, 202 | else: 203 | return cls.value, 204 | # TODO: 还什么都没实现... 205 | 206 | 207 | class HeroPowerCard(Card): 208 | @classmethod 209 | def get_card_type(cls): 210 | return CARD_HERO_POWER 211 | 212 | 213 | # 幸运币 214 | class Coin(SpellNoPoint): 215 | @classmethod 216 | def best_h_and_arg(cls, state, hand_card_index): 217 | best_delta_h = 0 218 | 219 | for another_index, hand_card in enumerate(state.my_hand_cards): 220 | delta_h = 0 221 | 222 | if hand_card.current_cost != state.my_last_mana + 1: 223 | continue 224 | if hand_card.is_coin: 225 | continue 226 | 227 | detail_card = hand_card.detail_card 228 | if detail_card is None: 229 | if hand_card.cardtype == CARD_MINION and not hand_card.battlecry: 230 | delta_h = MinionNoPoint.best_h_and_arg(state, another_index)[0] 231 | else: 232 | delta_h = detail_card.best_h_and_arg(state, another_index)[0] 233 | 234 | delta_h -= 2 # 如果跳费之后能使用的卡显著强于不跳费的卡, 就跳币 235 | best_delta_h = max(best_delta_h, delta_h) 236 | 237 | return best_delta_h, 238 | -------------------------------------------------------------------------------- /log_op.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import time 4 | import copy 5 | from constants.constants import * 6 | 7 | # "D 04:23:18.0000001 GameState.DebugPrintPower() - GameEntity EntityID=1" 8 | GAME_STATE_PATTERN = re.compile(r"D [\d]{2}:[\d]{2}:[\d]{2}.[\d]{7} GameState.DebugPrint(Game|Power)\(\) - (.+)") 9 | 10 | # "GameEntity EntityID=1" 11 | GAME_ENTITY_PATTERN = re.compile(r" *GameEntity EntityID=(\d+)") 12 | 13 | # "Player EntityID=2 PlayerID=1 GameAccountId=[hi=112233445566778899 lo=223344556]" 14 | PLAYER_PATTERN = re.compile(r" *Player EntityID=(\d+) PlayerID=(\d+).*") 15 | 16 | # "FULL_ENTITY - Creting ID=89 CardID=EX1_538t" 17 | # "FULL_ENTITY - Creating ID=90 CardID=" 18 | FULL_ENTITY_PATTERN = re.compile(r" *FULL_ENTITY - Creating ID=(\d+) CardID=(.*)") 19 | 20 | # "SHOW_ENTITY - Updating Entity=90 CardID=NEW1_033o" 21 | # "SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=32 zone=DECK zonePos=0 cardId= player=1] CardID=VAN_EX1_539" 22 | SHOW_ENTITY_PATTERN = re.compile(r" *SHOW_ENTITY - Updating Entity=(.*) CardID=(.*) *") 23 | 24 | # CHANGE_ENTITY 比较罕见,主要对应“呱”等变形行为 25 | # "CHANGE_ENTITY - Updating Entity=[entityName=凯恩·血蹄 id=37 zone=PLAY zonePos=3 cardId=VAN_EX1_110 player=2] CardID=hexfrog" 26 | CHANGE_ENTITY_PATTERN = re.compile(r" *CHANGE_ENTITY - Updating Entity=(.*) CardID=(.*) *") 27 | 28 | # "BLOCK_START BlockType=DEATHS Entity=GameEntity EffectCardId=System.Collections.Generic.List`1[System.String] EffectIndex=0 Target=0 SubOption=-1 " 29 | BLOCK_START_PATTERN = re.compile(r" *BLOCK_START BlockType=([A-Z]+) Entity=(.*) EffectCardId=.*") 30 | 31 | # "BLOCK_END" 32 | BlOCK_END_PATTERN = re.compile(r" *BLOCK_END *") 33 | 34 | # "PlayerID=1, PlayerName=UNKNOWN HUMAN PLAYER" 35 | # "PlayerID=2, PlayerName=Example#51234" 36 | PLAYER_ID_PATTERN = re.compile(r"PlayerID=(\d+), PlayerName=(.*)") 37 | 38 | # "TAG_CHANGE Entity=GameEntity tag=NEXT_STEP value=FINAL_WRAPUP " 39 | # "TAG_CHANGE Entity=Example#51234 tag=467 value=4 " 40 | # "TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=14 zone=DECK zonePos=0 cardId= player=1] tag=ZONE value=HAND " 41 | TAG_CHANGE_PATTERN = re.compile(r" *TAG_CHANGE Entity=(.*) tag=(.*) value=(.*) ") 42 | 43 | # "tag=ZONE value=DECK" 44 | TAG_PATTERN = re.compile(r" *tag=(.*) value=(.*)") 45 | 46 | 47 | class LineInfoContainer: 48 | def __init__(self, line_type, **kwargs): 49 | self.line_type = line_type 50 | self.info_dict = copy.copy(kwargs) 51 | 52 | def __str__(self): 53 | res = "line_type: " + str(self.line_type) + "\n" 54 | if len(self.info_dict) > 0: 55 | res += "info_dict\n" 56 | for key, value in self.info_dict.items(): 57 | res += "\t" + str(key) + ": " + str(value) + "\n" 58 | return res 59 | 60 | 61 | class LogInfoContainer: 62 | def __init__(self, log_type): 63 | self.log_type = log_type 64 | self.message_list = [] 65 | 66 | def append_info(self, line_info): 67 | self.message_list.append(line_info) 68 | 69 | @property 70 | def length(self): 71 | return len(self.message_list) 72 | 73 | 74 | def fetch_entity_id(input_string): 75 | if input_string[0] != "[": 76 | return input_string 77 | 78 | # 去除前后的 "[", "]" 79 | kv_list = input_string[1:-1] 80 | 81 | # 提取成形如 [... , "id=233" , ...]的格式 82 | kv_list = kv_list.split(" ") 83 | 84 | for item in kv_list: 85 | if item[:3] == "id=": 86 | return item[3:] 87 | 88 | 89 | def parse_line(line_str): 90 | match_obj = GAME_STATE_PATTERN.match(line_str) 91 | if match_obj is None: 92 | return 93 | 94 | line_str = match_obj.group(2) 95 | 96 | if line_str == "CREATE_GAME": 97 | return LineInfoContainer(LOG_LINE_CREATE_GAME) 98 | 99 | match_obj = TAG_CHANGE_PATTERN.match(line_str) 100 | if match_obj is not None: 101 | return LineInfoContainer( 102 | LOG_LINE_TAG_CHANGE, 103 | entity=fetch_entity_id(match_obj.group(1)), 104 | tag=match_obj.group(2), 105 | value=match_obj.group(3), 106 | ) 107 | 108 | match_obj = TAG_PATTERN.match(line_str) 109 | if match_obj is not None: 110 | return LineInfoContainer( 111 | LOG_LINE_TAG, 112 | tag=match_obj.group(1), 113 | value=match_obj.group(2) 114 | ) 115 | 116 | match_obj = GAME_ENTITY_PATTERN.match(line_str) 117 | if match_obj is not None: 118 | return LineInfoContainer( 119 | LOG_LINE_GAME_ENTITY, 120 | entity=match_obj.group(1) 121 | ) 122 | 123 | match_obj = PLAYER_PATTERN.match(line_str) 124 | if match_obj is not None: 125 | return LineInfoContainer( 126 | LOG_LINE_PLAYER_ENTITY, 127 | entity=match_obj.group(1), 128 | player=match_obj.group(2) 129 | ) 130 | 131 | match_obj = FULL_ENTITY_PATTERN.match(line_str) 132 | if match_obj is not None: 133 | return LineInfoContainer( 134 | LOG_LINE_FULL_ENTITY, 135 | entity=match_obj.group(1), 136 | card=match_obj.group(2) 137 | ) 138 | 139 | match_obj = SHOW_ENTITY_PATTERN.match(line_str) 140 | if match_obj is not None: 141 | return LineInfoContainer( 142 | LOG_LINE_SHOW_ENTITY, 143 | entity=fetch_entity_id(match_obj.group(1)), 144 | card=match_obj.group(2) 145 | ) 146 | 147 | match_obj = CHANGE_ENTITY_PATTERN.match(line_str) 148 | if match_obj is not None: 149 | return LineInfoContainer( 150 | LOG_LINE_CHANGE_ENTITY, 151 | entity=fetch_entity_id(match_obj.group(1)), 152 | card=match_obj.group(2) 153 | ) 154 | 155 | match_obj = BLOCK_START_PATTERN.match(line_str) 156 | if match_obj is not None: 157 | return LineInfoContainer( 158 | LOG_LINE_BLOCK_START, 159 | type=match_obj.group(1), 160 | card=match_obj.group(2) 161 | ) 162 | 163 | match_obj = BlOCK_END_PATTERN.match(line_str) 164 | if match_obj is not None: 165 | return LineInfoContainer( 166 | LOG_LINE_BLOCK_END 167 | ) 168 | 169 | match_obj = PLAYER_ID_PATTERN.match(line_str) 170 | if match_obj is not None: 171 | return LineInfoContainer( 172 | LOG_LINE_PLAYER_ID, 173 | player=match_obj.group(1), 174 | name=match_obj.group(2) 175 | ) 176 | 177 | return None 178 | 179 | 180 | def log_iter_func(path=HEARTHSTONE_POWER_LOG_PATH): 181 | while True: 182 | if not os.path.exists(path): 183 | yield LogInfoContainer(LOG_CONTAINER_ERROR) 184 | continue 185 | 186 | with open(path, "r", encoding="utf8") as f: 187 | while True: 188 | 189 | empty_line_count = 0 190 | log_container = LogInfoContainer(LOG_CONTAINER_INFO) 191 | 192 | while True: 193 | line = f.readline() 194 | 195 | if line == "": 196 | time.sleep(0.2) 197 | empty_line_count += 1 198 | if empty_line_count == 2: 199 | break 200 | else: 201 | empty_line_count = 0 202 | line_container = parse_line(line) 203 | if line_container is not None: 204 | log_container.append_info(line_container) 205 | 206 | yield log_container 207 | 208 | if not os.path.exists(path): 209 | yield LogInfoContainer(LOG_CONTAINER_ERROR) 210 | break 211 | 212 | 213 | if __name__ == "__main__": 214 | line_str = input() 215 | print(parse_line(line_str)) 216 | -------------------------------------------------------------------------------- /click.py: -------------------------------------------------------------------------------- 1 | import win32gui 2 | import win32api 3 | import win32con 4 | import pywintypes 5 | import time 6 | from pynput.mouse import Button, Controller 7 | import random 8 | # import get_screen 9 | import sys 10 | 11 | from constants.constants import * 12 | from print_info import * 13 | from get_screen import * 14 | 15 | 16 | #测试不同分辨率的点击效果,因为屏幕截取未支持不同分辨率,失败了 17 | #炉石传说坐标 18 | # hwnd_left,hwnd_top, hwnd_right, hwnd_bottom = 0,0,0,0 19 | # hwnd_width = 0 20 | # hwnd_height = 0 21 | 22 | #获取炉石传说的分辨率 23 | #得到不同分辨率下的点击位置 24 | # def hs_size(): 25 | # hwnd = get_HS_hwnd() 26 | # global hwnd_left 27 | # global hwnd_top 28 | # global hwnd_right 29 | # global hwnd_bottom 30 | # hwnd_left,hwnd_top,hwnd_right,hwnd_bottom = win32gui.GetWindowRect(hwnd) 31 | # # print(left) 32 | # # print(top) 33 | # # print(right) 34 | # # print(bottom) 35 | # global hwnd_width 36 | # global hwnd_height 37 | # hwnd_width = hwnd_right - hwnd_left -16 38 | # hwnd_height = hwnd_bottom - hwnd_top -39 39 | # print(hwnd_width) 40 | # print(hwnd_height) 41 | # # 1440 900 = 1456 939 42 | # # 16 39 43 | # # 1400 1050 = 1416 1089 44 | # # 16 39 45 | 46 | # def position_x(x): 47 | # global hwnd_width 48 | # global hwnd_left 49 | # if hwnd_width == 0: 50 | # return x 51 | # return x * (hwnd_width / 1920) + 16 + hwnd_left 52 | 53 | # def position_y(y): 54 | # global hwnd_height 55 | # global hwnd_top 56 | # if hwnd_height == 0: 57 | # return y 58 | # return y * (hwnd_height / 1920) + 39 + hwnd_top 59 | 60 | 61 | 62 | def rand_sleep(interval): 63 | base_time = interval * 0.75 64 | rand_time = interval * 0.5 * random.random() # avg = 0.25 * interval 65 | time.sleep(base_time + rand_time) 66 | 67 | 68 | def click_button(x, y, button): 69 | # x = position_x(x) 70 | # y = position_y(y) 71 | mouse = Controller() 72 | rand_sleep(0.1) 73 | mouse.position = (x, y) 74 | rand_sleep(0.1) 75 | mouse.press(button) 76 | rand_sleep(0.1) 77 | mouse.release(button) 78 | 79 | 80 | def left_click(x, y): 81 | x += random.randint(-2, 3) 82 | y += random.randint(-2, 3) 83 | click_button(x, y, Button.left) 84 | 85 | 86 | def right_click(x, y): 87 | click_button(x, y, Button.right) 88 | 89 | 90 | def choose_my_minion(mine_index, mine_num): 91 | rand_sleep(OPERATE_INTERVAL) 92 | x = 960 - (mine_num - 1) * 70 + mine_index * 140 93 | y = 600 94 | left_click(x, y) 95 | 96 | 97 | def choose_my_hero(): 98 | rand_sleep(OPERATE_INTERVAL) 99 | left_click(960, 850) 100 | 101 | 102 | def choose_opponent_minion(oppo_index, oppo_num): 103 | rand_sleep(OPERATE_INTERVAL) 104 | x = 960 - (oppo_num - 1) * 70 + oppo_index * 140 105 | y = 400 106 | left_click(x, y) 107 | 108 | 109 | def choose_oppo_hero(): 110 | rand_sleep(OPERATE_INTERVAL) 111 | left_click(960, 200) 112 | 113 | 114 | def cancel_click(): 115 | rand_sleep(TINY_OPERATE_INTERVAL) 116 | right_click(50, 400) 117 | 118 | 119 | def test_click(): 120 | rand_sleep(TINY_OPERATE_INTERVAL) 121 | left_click(50, 400) 122 | 123 | 124 | HAND_CARD_X = [ 125 | [], # 0 126 | [885], # 1 127 | [820, 980], # 2 128 | [750, 890, 1040], # 3 129 | [690, 820, 970, 1130], # 4 130 | [680, 780, 890, 1010, 1130], # 5 131 | [660, 750, 840, 930, 1020, 1110], # 6 132 | [660, 733, 810, 885, 965, 1040, 1120], # 7 133 | [650, 720, 785, 855, 925, 995, 1060, 1130], # 8 134 | [650, 710, 765, 825, 880, 950, 1010, 1070, 1140], # 9 135 | [647, 700, 750, 800, 860, 910, 970, 1020, 1070, 1120] # 10 136 | ] 137 | 138 | 139 | def choose_card(card_index, card_num): 140 | rand_sleep(OPERATE_INTERVAL) 141 | 142 | assert 0 <= card_index < card_num <= 10 143 | # x = START[card_num] + 65 + STEP[card_num] * card_index 144 | x = HAND_CARD_X[card_num][card_index] 145 | 146 | y = 1000 147 | left_click(x, y) 148 | 149 | 150 | STARTING_CARD_X = { 151 | 3: [600, 960, 1320], 152 | 5: [600, 850, 1100, 1350], 153 | } 154 | 155 | 156 | def replace_starting_card(card_index, hand_card_num): 157 | assert hand_card_num in STARTING_CARD_X 158 | assert card_index < len(STARTING_CARD_X[hand_card_num]) 159 | 160 | rand_sleep(OPERATE_INTERVAL) 161 | left_click(STARTING_CARD_X[hand_card_num][card_index], 500) 162 | 163 | 164 | def click_middle(): 165 | rand_sleep(OPERATE_INTERVAL) 166 | left_click(960, 500) 167 | 168 | 169 | def click_setting(): 170 | rand_sleep(OPERATE_INTERVAL) 171 | left_click(1880, 1050) 172 | 173 | 174 | def choose_and_use_spell(card_index, card_num): 175 | choose_card(card_index, card_num) 176 | click_middle() 177 | 178 | 179 | # 第[i]个随从左边那个空隙记为第[i]个gap 180 | def put_minion(gap_index, minion_num): 181 | rand_sleep(OPERATE_INTERVAL) 182 | 183 | if minion_num >= 7: 184 | warn_print(f"Try to put a minion but there has already been {minion_num} minions") 185 | 186 | x = 960 - (minion_num - 1) * 70 + 140 * gap_index - 70 187 | y = 600 188 | left_click(x, y) 189 | 190 | 191 | def match_opponent(): 192 | # 一些奇怪的错误提示 193 | commit_error_report() 194 | rand_sleep(OPERATE_INTERVAL) 195 | left_click(1400, 900) 196 | 197 | 198 | def enter_battle_mode(): 199 | # 一些奇怪的错误提示 200 | commit_error_report() 201 | rand_sleep(OPERATE_INTERVAL) 202 | left_click(950, 320) 203 | 204 | 205 | def commit_choose_card(): 206 | rand_sleep(OPERATE_INTERVAL) 207 | left_click(960, 850) 208 | 209 | 210 | def end_turn(): 211 | rand_sleep(OPERATE_INTERVAL) 212 | left_click(1550, 500) 213 | 214 | 215 | def commit_error_report(): 216 | # 一些奇怪的错误提示 217 | left_click(1100, 820) 218 | # 如果已断线, 点这里时取消 219 | left_click(960, 650) 220 | 221 | 222 | def emoj(target=None): 223 | emoj_list = [(800, 880), (800, 780), (800, 680), (1150, 680), (1150, 780)] 224 | right_click(960, 830) 225 | rand_sleep(OPERATE_INTERVAL) 226 | 227 | if target is None: 228 | x, y = emoj_list[random.randint(1, 4)] 229 | else: 230 | x, y = emoj_list[target] 231 | left_click(x, y) 232 | rand_sleep(OPERATE_INTERVAL) 233 | 234 | 235 | def click_skill(): 236 | rand_sleep(OPERATE_INTERVAL) 237 | left_click(1150, 850) 238 | 239 | 240 | def use_skill_no_point(): 241 | click_skill() 242 | cancel_click() 243 | 244 | 245 | def use_skill_point_mine(my_index, my_num): 246 | click_skill() 247 | 248 | if my_index < 0: 249 | choose_my_hero() 250 | else: 251 | choose_my_minion(my_index, my_num) 252 | 253 | cancel_click() 254 | 255 | 256 | def minion_beat_minion(mine_index, mine_number, oppo_index, oppo_num): 257 | choose_my_minion(mine_index, mine_number) 258 | choose_opponent_minion(oppo_index, oppo_num) 259 | cancel_click() 260 | 261 | 262 | def minion_beat_hero(mine_index, mine_number): 263 | choose_my_minion(mine_index, mine_number) 264 | choose_oppo_hero() 265 | cancel_click() 266 | 267 | 268 | def hero_beat_minion(oppo_index, oppo_num): 269 | choose_my_hero() 270 | choose_opponent_minion(oppo_index, oppo_num) 271 | cancel_click() 272 | 273 | 274 | def hero_beat_hero(): 275 | choose_my_hero() 276 | choose_oppo_hero() 277 | cancel_click() 278 | 279 | 280 | def enter_HS(): 281 | rand_sleep(1) 282 | 283 | if test_hs_available(): 284 | move_window_foreground(get_HS_hwnd(), "炉石传说") 285 | return 286 | 287 | battlenet_hwnd = get_battlenet_hwnd() 288 | 289 | if battlenet_hwnd == 0: 290 | error_print("未找到应用战网") 291 | sys.exit() 292 | 293 | move_window_foreground(battlenet_hwnd, "战网") 294 | 295 | rand_sleep(1) 296 | 297 | left, top, right, bottom = win32gui.GetWindowRect(battlenet_hwnd) 298 | left_click(left + 180, bottom - 110) 299 | -------------------------------------------------------------------------------- /card/classic_card.py: -------------------------------------------------------------------------------- 1 | from card.basic_card import * 2 | 3 | 4 | # 闪电箭 5 | class LightingBolt(SpellPointOppo): 6 | spell_type = SPELL_POINT_OPPO 7 | bias = -4 8 | 9 | @classmethod 10 | def best_h_and_arg(cls, state, hand_card_index): 11 | spell_power = state.my_total_spell_power 12 | damage = 3 + spell_power 13 | best_delta_h = state.oppo_hero.delta_h_after_damage(damage) 14 | best_oppo_index = -1 15 | 16 | for oppo_index, oppo_minion in enumerate(state.oppo_minions): 17 | if not oppo_minion.can_be_pointed_by_spell: 18 | continue 19 | delta_h = oppo_minion.delta_h_after_damage(damage) 20 | if best_delta_h < delta_h: 21 | best_delta_h = delta_h 22 | best_oppo_index = oppo_index 23 | 24 | return best_delta_h + cls.bias, best_oppo_index, 25 | 26 | 27 | # 呱 28 | class Hex(SpellPointOppo): 29 | bias = -6 30 | keep_in_hand_bool = False 31 | 32 | @classmethod 33 | def best_h_and_arg(cls, state, hand_card_index): 34 | best_delta_h = 0 35 | best_oppo_index = -1 36 | 37 | for oppo_index, oppo_minion in enumerate(state.oppo_minions): 38 | if not oppo_minion.can_be_pointed_by_spell: 39 | continue 40 | 41 | delta_h = oppo_minion.heuristic_val - 1 42 | 43 | if best_delta_h < delta_h: 44 | best_delta_h = delta_h 45 | best_oppo_index = oppo_index 46 | 47 | return best_delta_h + cls.bias, best_oppo_index, 48 | 49 | 50 | # 闪电风暴 51 | class LightningStorm(SpellNoPoint): 52 | bias = -10 53 | 54 | @classmethod 55 | def best_h_and_arg(cls, state, hand_card_index): 56 | h_sum = 0 57 | spell_power = state.my_total_spell_power 58 | 59 | for oppo_minion in state.oppo_minions: 60 | h_sum += (oppo_minion.delta_h_after_damage(2 + spell_power) + 61 | oppo_minion.delta_h_after_damage(3 + spell_power)) / 2 62 | 63 | return h_sum + cls.bias, 64 | 65 | 66 | # TC130 67 | class MindControlTech(MinionNoPoint): 68 | value = 0.2 69 | keep_in_hand_bool = False 70 | 71 | @classmethod 72 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 73 | if state.oppo_minion_num < 4: 74 | return cls.value, state.my_minion_num 75 | else: 76 | h_sum = sum([minion.heuristic_val for minion in state.oppo_minions]) 77 | h_sum /= state.oppo_minion_num 78 | return cls.value + h_sum * 2, 79 | 80 | 81 | # 野性狼魂 82 | class FeralSpirit(SpellNoPoint): 83 | value = 2.4 84 | 85 | @classmethod 86 | def best_h_and_arg(cls, state, hand_card_index): 87 | if state.my_minion_num >= 7: 88 | return -1, 0 89 | else: 90 | return cls.value, 0 91 | 92 | 93 | # 碧蓝幼龙 94 | class AzureDrake(MinionNoPoint): 95 | value = 3.5 96 | keep_in_hand_bool = False 97 | 98 | 99 | # 奥妮克希亚 100 | class Onyxia(MinionNoPoint): 101 | value = 10 102 | keep_in_hand_bool = False 103 | 104 | 105 | # 火元素 106 | class FireElemental(MinionPointOppo): 107 | keep_in_hand_bool = False 108 | 109 | @classmethod 110 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 111 | best_h = 3 + state.oppo_hero.delta_h_after_damage(3) 112 | best_oppo_index = -1 113 | 114 | for oppo_index, oppo_minion in enumerate(state.oppo_minions): 115 | if not oppo_minion.can_be_pointed_by_minion: 116 | continue 117 | 118 | delta_h = 3 + oppo_minion.delta_h_after_damage(3) 119 | if delta_h > best_h: 120 | best_h = delta_h 121 | best_oppo_index = oppo_index 122 | 123 | return best_h, state.my_minion_num, best_oppo_index 124 | 125 | 126 | # 精灵弓箭手 127 | class ElvenArcher(MinionPointOppo): 128 | @classmethod 129 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 130 | # 不能让她下去点脸, 除非对面快死了 131 | best_h = -0.8 + state.oppo_hero.delta_h_after_damage(1) 132 | best_oppo_index = -1 133 | 134 | for oppo_index, oppo_minion in enumerate(state.oppo_minions): 135 | if not oppo_minion.can_be_pointed_by_minion: 136 | continue 137 | 138 | delta_h = -0.5 + oppo_minion.delta_h_after_damage(1) 139 | if delta_h > best_h: 140 | best_h = delta_h 141 | best_oppo_index = oppo_index 142 | 143 | return best_h, state.my_minion_num, best_oppo_index 144 | 145 | 146 | # 大地之环先知 147 | class EarthenRingFarseer(MinionPointMine): 148 | @classmethod 149 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 150 | best_h = 0.2 + state.my_hero.delta_h_after_heal(3) 151 | if state.my_hero.health <= 5: 152 | best_h += 4 153 | best_my_index = -1 154 | 155 | for my_index, my_minion in enumerate(state.my_minions): 156 | delta_h = -0.5 + my_minion.delta_h_after_heal(3) 157 | if delta_h > best_h: 158 | best_h = delta_h 159 | best_my_index = my_index 160 | 161 | return best_h, state.my_minion_num, best_my_index 162 | 163 | 164 | # 憎恶 165 | class Abomination(MinionNoPoint): 166 | keep_in_hand_bool = True 167 | 168 | @classmethod 169 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 170 | h_sum = 0 171 | for oppo_minion in state.oppo_minions: 172 | h_sum += oppo_minion.delta_h_after_damage(2) 173 | for my_minion in state.my_minions: 174 | h_sum -= my_minion.delta_h_after_damage(2) 175 | h_sum += state.oppo_hero.delta_h_after_damage(2) 176 | h_sum -= state.my_hero.delta_h_after_damage(2) 177 | 178 | return h_sum, 179 | 180 | 181 | # 狂奔科多兽 182 | class StampedingKodo(MinionNoPoint): 183 | keep_in_hand_bool = False 184 | 185 | @classmethod 186 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 187 | h_sum = 2 188 | temp_sum = 0 189 | temp_count = 0 190 | 191 | for oppo_minion in state.oppo_minions: 192 | if oppo_minion.attack <= 2: 193 | temp_sum += oppo_minion.heuristic_val 194 | temp_count += 1 195 | if temp_count > 0: 196 | h_sum += temp_sum / temp_count 197 | 198 | return h_sum, 199 | 200 | 201 | # 血骑士 202 | class BloodKnight(MinionNoPoint): 203 | keep_in_hand_bool = False 204 | 205 | @classmethod 206 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 207 | h_sum = 1 208 | 209 | for oppo_minion in state.oppo_minions: 210 | if oppo_minion.divine_shield: 211 | h_sum += oppo_minion.attack + 6 212 | for my_minion in state.my_minions: 213 | if my_minion.divine_shield: 214 | h_sum += -my_minion.attack + 6 215 | 216 | return h_sum, 217 | 218 | 219 | # 末日 220 | class DoomSayer(MinionNoPoint): 221 | keep_in_hand_bool = True 222 | 223 | @classmethod 224 | def utilize_delta_h_and_arg(cls, state, hand_card_index): 225 | # 一费别跳末日 226 | if state.my_total_mana == 1: 227 | return 0, 228 | 229 | # 二三费压末日就完事了 230 | if state.my_total_mana <= 3: 231 | return 1000, 232 | 233 | # 优势不能上末日 234 | if state.my_heuristic_value >= state.oppo_heuristic_value: 235 | return 0, 236 | 237 | oppo_attack_sum = 0 238 | for oppo_minion in state.oppo_minions: 239 | oppo_attack_sum += oppo_minion.attack 240 | 241 | if oppo_attack_sum >= 7: 242 | # 当个嘲讽也好 243 | return 1, 244 | else: 245 | return state.oppo_heuristic_value - state.my_heuristic_value, 246 | 247 | 248 | class StormforgedAxe(WeaponCard): 249 | keep_in_hand_bool = True 250 | value = 1.5 251 | 252 | @classmethod 253 | def best_h_and_arg(cls, state, hand_card_index): 254 | # 不要已经有刀了再顶刀 255 | if state.my_weapon is not None: 256 | return 0, 257 | if state.my_total_mana == 2: 258 | for oppo_minion in state.touchable_oppo_minions: 259 | # 如果能提起刀解了, 那太好了 260 | if oppo_minion.health <= 2 and \ 261 | not oppo_minion.divine_shield: 262 | return 2000, 263 | 264 | return cls.value, 265 | -------------------------------------------------------------------------------- /card/standard_card.py: -------------------------------------------------------------------------------- 1 | from card.basic_card import * 2 | 3 | 4 | # 护甲商贩 5 | class ArmorVendor(MinionNoPoint): 6 | value = 2 7 | keep_in_hand_bool = True 8 | 9 | 10 | # 神圣惩击 11 | class HolySmite(SpellPointOppo): 12 | wait_time = 2 13 | # 加个bias,一是包含了消耗的水晶的代价,二是包含了消耗了手牌的代价 14 | bias = -2 15 | 16 | @classmethod 17 | def best_h_and_arg(cls, state, hand_card_index): 18 | best_oppo_index = -1 19 | best_delta_h = 0 20 | 21 | for oppo_index, oppo_minion in enumerate(state.oppo_minions): 22 | if not oppo_minion.can_be_pointed_by_spell: 23 | continue 24 | temp_delta_h = oppo_minion.delta_h_after_damage(3) + cls.bias 25 | if temp_delta_h > best_delta_h: 26 | best_delta_h = temp_delta_h 27 | best_oppo_index = oppo_index 28 | 29 | return best_delta_h, best_oppo_index 30 | 31 | 32 | # 倦怠光波 33 | class WaveOfApathy(SpellNoPoint): 34 | wait_time = 2 35 | bias = -4 36 | 37 | @classmethod 38 | def best_h_and_arg(cls, state, hand_card_index): 39 | tmp = 0 40 | 41 | for minion in state.oppo_minions: 42 | tmp += minion.attack - 1 43 | 44 | return tmp + cls.bias, 45 | 46 | 47 | # 噬骨殴斗者 48 | class BonechewerBrawler(MinionNoPoint): 49 | value = 2 50 | keep_in_hand_bool = True 51 | 52 | 53 | # 暗言术灭 54 | class ShadowWordDeath(SpellPointOppo): 55 | wait_time = 1.5 56 | bias = -6 57 | 58 | @classmethod 59 | def best_h_and_arg(cls, state, hand_card_index): 60 | best_oppo_index = -1 61 | best_delta_h = 0 62 | 63 | for oppo_index, oppo_minion in enumerate(state.oppo_minions): 64 | if oppo_minion.attack < 5: 65 | continue 66 | if not oppo_minion.can_be_pointed_by_spell: 67 | continue 68 | 69 | tmp = oppo_minion.heuristic_val + cls.bias 70 | if tmp > best_delta_h: 71 | best_delta_h = tmp 72 | best_oppo_index = oppo_index 73 | 74 | return best_delta_h, best_oppo_index 75 | 76 | 77 | # 神圣化身 78 | class Apotheosis(SpellPointMine): 79 | bias = -6 80 | 81 | @classmethod 82 | def best_h_and_arg(cls, state, hand_card_index): 83 | best_delta_h = 0 84 | best_mine_index = -1 85 | 86 | for my_index, my_minion in enumerate(state.my_minions): 87 | if not my_minion.can_be_pointed_by_spell: 88 | continue 89 | 90 | tmp = cls.bias + 3 + (my_minion.health + 2) / 4 + \ 91 | (my_minion.attack + 1) / 2 92 | if my_minion.can_attack_minion: 93 | tmp += my_minion.attack / 4 94 | if tmp > best_delta_h: 95 | best_delta_h = tmp 96 | best_mine_index = my_index 97 | 98 | return best_delta_h, best_mine_index 99 | 100 | 101 | # 亡首教徒 102 | class DeathsHeadCultist(MinionNoPoint): 103 | value = 1 104 | keep_in_hand_bool = True 105 | 106 | 107 | # 噬灵疫病 108 | class DevouringPlague(SpellNoPoint): 109 | wait_time = 4 110 | bias = -4 # 把吸的血直接算进bias 111 | 112 | @classmethod 113 | def best_h_and_arg(cls, state, hand_card_index): 114 | curr_h = state.heuristic_value 115 | 116 | delta_h_sum = 0 117 | sample_times = 5 118 | 119 | for i in range(sample_times): 120 | tmp_state = state.copy_new_one() 121 | for j in range(4): 122 | tmp_state.random_distribute_damage(1, [i for i in range(tmp_state.oppo_minion_num)], []) 123 | 124 | delta_h_sum += tmp_state.heuristic_value - curr_h 125 | 126 | return delta_h_sum / sample_times + cls.bias, 127 | 128 | 129 | # 狂傲的兽人 130 | class OverconfidentOrc(MinionNoPoint): 131 | value = 3 132 | keep_in_hand_bool = True 133 | 134 | 135 | # 神圣新星 136 | class HolyNova(SpellNoPoint): 137 | bias = -8 138 | 139 | @classmethod 140 | def best_h_and_arg(cls, state, hand_card_index): 141 | return cls.bias + sum([minion.delta_h_after_damage(2) 142 | for minion in state.oppo_minions]), 143 | 144 | 145 | # 狂乱 146 | class Hysteria(SpellPointOppo): 147 | wait_time = 5 148 | bias = -9 # 我觉得狂乱应该要能力挽狂澜 149 | keep_in_hand_bool = False 150 | 151 | @classmethod 152 | def best_h_and_arg(cls, state, hand_card_index): 153 | best_delta_h = 0 154 | best_arg = 0 155 | sample_times = 10 156 | 157 | if state.oppo_minion_num == 0 or state.oppo_minion_num + state.my_minion_num == 1: 158 | return 0, -1 159 | 160 | for chosen_index, chosen_minion in enumerate(state.oppo_minions): 161 | if not chosen_minion.can_be_pointed_by_spell: 162 | continue 163 | 164 | delta_h_count = 0 165 | 166 | for i in range(sample_times): 167 | tmp_state = state.copy_new_one() 168 | tmp_chosen_index = chosen_index 169 | 170 | while True: 171 | another_index_list = [j for j in range(tmp_state.oppo_minion_num + tmp_state.my_minion_num)] 172 | another_index_list.pop(tmp_chosen_index) 173 | if len(another_index_list) == 0: 174 | break 175 | another_index = another_index_list[random.randint(0, len(another_index_list) - 1)] 176 | 177 | # print("another index: ", another_index) 178 | if another_index >= tmp_state.oppo_minion_num: 179 | another_minion = tmp_state.my_minions[another_index - tmp_state.oppo_minion_num] 180 | if another_minion.get_damaged(chosen_minion.attack): 181 | tmp_state.my_minions.pop(another_index - tmp_state.oppo_minion_num) 182 | else: 183 | another_minion = tmp_state.oppo_minions[another_index] 184 | if another_minion.get_damaged(chosen_minion.attack): 185 | tmp_state.oppo_minions.pop(another_index) 186 | if another_index < tmp_chosen_index: 187 | tmp_chosen_index -= 1 188 | 189 | if chosen_minion.get_damaged(another_minion.attack): 190 | # print("h:", tmp_state.heuristic_value, state.heuristic_value) 191 | tmp_state.oppo_minions.pop(tmp_chosen_index) 192 | break 193 | 194 | # print("h:", tmp_state.heuristic_value, state.heuristic_value) 195 | 196 | delta_h_count += tmp_state.heuristic_value - state.heuristic_value 197 | 198 | delta_h_count /= sample_times 199 | # print("average delta_h:", delta_h_count) 200 | if delta_h_count > best_delta_h: 201 | best_delta_h = delta_h_count 202 | best_arg = chosen_index 203 | 204 | return best_delta_h + cls.bias, best_arg 205 | 206 | 207 | # 暗言术毁 208 | class ShadowWordRuin(SpellNoPoint): 209 | bias = -8 210 | keep_in_hand_bool = False 211 | 212 | @classmethod 213 | def best_h_and_arg(cls, state, hand_card_index): 214 | return cls.bias + sum([minion.heuristic_val 215 | for minion in state.oppo_minions 216 | if minion.attack >= 5]), 217 | 218 | 219 | # 除奇致胜 220 | class AgainstAllOdds(SpellNoPoint): 221 | bias = -9 222 | keep_in_hand_bool = False 223 | 224 | @classmethod 225 | def best_h_and_arg(cls, state, hand_card_index): 226 | return cls.bias + \ 227 | sum([minion.heuristic_val 228 | for minion in state.oppo_minions 229 | if minion.attack % 2 == 1]) - \ 230 | sum([minion.heuristic_val 231 | for minion in state.my_minions 232 | if minion.attack % 2 == 1]), 233 | 234 | 235 | # 锈骑劫匪 236 | class RuststeedRaider(MinionNoPoint): 237 | value = 3 238 | keep_in_hand_bool = False 239 | # TODO: 也许我可以为突袭随从专门写一套价值评判? 240 | 241 | 242 | # 泰兰佛丁 243 | class TaelanFordring(MinionNoPoint): 244 | value = 3 245 | keep_in_hand_bool = False 246 | 247 | 248 | # 凯恩血蹄 249 | class CairneBloodhoof(MinionNoPoint): 250 | value = 6 251 | keep_in_hand_bool = False 252 | 253 | 254 | # 吃手手鱼 255 | class MutanusTheDevourer(MinionNoPoint): 256 | value = 5 257 | keep_in_hand_bool = False 258 | 259 | 260 | # 灵魂之镜 261 | class SoulMirror(SpellNoPoint): 262 | wait_time = 5 263 | bias = -16 264 | keep_in_hand_bool = False 265 | 266 | @classmethod 267 | def best_h_and_arg(cls, state, hand_card_index): 268 | copy_number = min(7 - state.my_minion_num, state.oppo_minion_num) 269 | h_sum = 0 270 | for i in range(copy_number): 271 | h_sum += state.oppo_minions[i].heuristic_val 272 | 273 | return h_sum + cls.bias, 274 | 275 | 276 | # 戈霍恩之血 277 | class BloodOfGhuun(MinionNoPoint): 278 | value = 8 279 | keep_in_hand_bool = False 280 | -------------------------------------------------------------------------------- /strategy_entity.py: -------------------------------------------------------------------------------- 1 | from json_op import * 2 | from abc import abstractmethod 3 | from card.id2card import ID2CARD_DICT 4 | import copy 5 | 6 | 7 | class StrategyEntity: 8 | def __init__(self, card_id, zone, zone_pos, 9 | current_cost, overload, is_mine): 10 | self.card_id = card_id 11 | self.zone = zone 12 | self.zone_pos = zone_pos 13 | self.current_cost = current_cost 14 | self.overload = overload 15 | self.is_mine = is_mine 16 | 17 | @property 18 | def name(self): 19 | return query_json_dict(self.card_id) 20 | 21 | @property 22 | def heuristic_val(self): 23 | return 0 24 | 25 | @property 26 | @abstractmethod 27 | def cardtype(self): 28 | pass 29 | 30 | @property 31 | def is_coin(self): 32 | return self.name == "幸运币" 33 | 34 | @property 35 | def detail_card(self): 36 | if self.is_coin: 37 | return ID2CARD_DICT["COIN"] 38 | else: 39 | return ID2CARD_DICT.get(self.card_id, None) 40 | 41 | # uni_index是对场上可能要被鼠标指到的对象的统一编号. 42 | # 包括敌我随从和敌我英雄, 具体编号为: 43 | # 0-6: 我方随从 44 | # 9: 我方英雄 45 | # 10-16: 敌方随从 46 | # 19: 敌方英雄  47 | @property 48 | def uni_index(self): 49 | return -1 50 | 51 | 52 | CRITICAL_MINION = { 53 | "VAN_NEW1_019": 1.5, # 飞刀杂耍者 54 | "VAN_EX1_162": 1.5, # 恐狼前锋 55 | "VAN_CS2_235": 1.5, # 北郡牧师 56 | "VAN_CS2_237": 2, # 饥饿的秃鹫 57 | "VAN_EX1_004": 1.5, # 年轻的女祭司 58 | "VAN_EX1_095": 1.5, # 加基森拍卖师 59 | "VAN_EX1_044": 1.5, # 任务达人 60 | } 61 | 62 | 63 | class StrategyMinion(StrategyEntity): 64 | def __init__(self, card_id, zone, zone_pos, 65 | current_cost, overload, is_mine, 66 | attack, max_health, damage=0, 67 | taunt=0, divine_shield=0, stealth=0, 68 | windfury=0, poisonous=0, life_steal=0, 69 | spell_power=0, freeze=0, battlecry=0, 70 | not_targeted_by_spell=0, not_targeted_by_power=0, 71 | charge=0, rush=0, 72 | attackable_by_rush=0, frozen=0, 73 | dormant=0, untouchable=0, immune=0, 74 | cant_attack=0, exhausted=1, num_turns_in_play=1): 75 | super().__init__(card_id, zone, zone_pos, 76 | current_cost, overload, is_mine) 77 | self.attack = attack 78 | self.max_health = max_health 79 | self.damage = damage 80 | self.taunt = taunt 81 | self.divine_shield = divine_shield 82 | self.stealth = stealth 83 | self.windfury = windfury 84 | self.poisonous = poisonous 85 | self.life_steal = life_steal 86 | self.spell_power = spell_power 87 | self.freeze = freeze 88 | self.battlecry = battlecry 89 | self.not_targeted_by_spell = not_targeted_by_spell 90 | self.not_targeted_by_power = not_targeted_by_power 91 | self.charge = charge 92 | self.rush = rush 93 | # 当一个随从具有毛刺绿边(就是突袭随从刚出来时的绿边)的时候就会有这个属性 94 | self.attackable_by_rush = attackable_by_rush 95 | self.frozen = frozen 96 | self.dormant = dormant 97 | # UNTOUCHABLE作用不明, 不过休眠的随从会在休眠时具有UNTOUCHABLE属性 98 | # self.untouchable = untouchable 99 | self.immune = immune 100 | self.cant_attack = cant_attack 101 | self.num_turns_in_play = num_turns_in_play 102 | # exhausted == 1: 随从没有绿边, 不能动 103 | # 普通随从一入场便具有 exhausted == 1, 104 | # 但是突袭随从和冲锋随从一开始不具有这个标签, 105 | # 所以还要另作判断(尤其是突袭随从一开始不能打脸) 106 | self.exhausted = exhausted 107 | 108 | # 对于突袭随从, 第一回合应不能打脸, 而能攻击随从由 109 | # attackable_by_rush体现 110 | if self.rush and not self.charge \ 111 | and self.num_turns_in_play < 2: 112 | self.exhausted = 1 113 | 114 | def __str__(self): 115 | temp = f"[{self.zone_pos}] {self.name} " \ 116 | f"{self.attack}-{self.health}({self.max_health})" 117 | 118 | if self.can_beat_face: 119 | temp += " [能打脸]" 120 | elif self.can_attack_minion: 121 | temp += " [能打怪]" 122 | else: 123 | temp += " [不能动]" 124 | 125 | if self.dormant: 126 | temp += " 休眠" 127 | if self.immune: 128 | temp += " 免疫" 129 | if self.frozen: 130 | temp += " 被冻结" 131 | if self.taunt: 132 | temp += " 嘲讽" 133 | if self.divine_shield: 134 | temp += " 圣盾" 135 | if self.stealth: 136 | temp += " 潜行" 137 | if self.charge: 138 | temp += " 冲锋" 139 | if self.rush: 140 | temp += " 突袭" 141 | if self.windfury: 142 | temp += " 风怒" 143 | if self.poisonous: 144 | temp += " 剧毒" 145 | if self.life_steal: 146 | temp += " 吸血" 147 | if self.freeze: 148 | temp += " 冻结敌人" 149 | if self.not_targeted_by_spell and self.not_targeted_by_power: 150 | temp += " 魔免" 151 | if self.spell_power: 152 | temp += f" 法术伤害+{self.spell_power}" 153 | if self.cant_attack: 154 | temp += " 不能攻击" 155 | 156 | temp += f" h_val:{self.heuristic_val}" 157 | 158 | return temp 159 | 160 | @property 161 | def cardtype(self): 162 | return CARD_MINION 163 | 164 | @property 165 | def uni_index(self): 166 | if self.is_mine: 167 | return self.zone_pos - 1 168 | else: 169 | return self.zone_pos - 1 + 10 170 | 171 | @property 172 | def health(self): 173 | return self.max_health - self.damage 174 | 175 | @property 176 | def can_beat_face(self): 177 | return self.attack > 0 \ 178 | and not self.dormant \ 179 | and not self.frozen \ 180 | and not self.cant_attack \ 181 | and self.exhausted == 0 182 | 183 | @property 184 | def can_attack_minion(self): 185 | return self.attack > 0 \ 186 | and not self.dormant \ 187 | and not self.frozen\ 188 | and not self.cant_attack \ 189 | and (self.exhausted == 0 190 | or self.attackable_by_rush) 191 | 192 | @property 193 | def can_be_pointed_by_spell(self): 194 | return not self.stealth \ 195 | and not self.not_targeted_by_spell \ 196 | and not self.dormant \ 197 | and not self.immune 198 | 199 | @property 200 | def can_be_pointed_by_hero_power(self): 201 | return not self.stealth \ 202 | and not self.not_targeted_by_power \ 203 | and not self.dormant \ 204 | and not self.immune 205 | 206 | @property 207 | def can_be_pointed_by_minion(self): 208 | return not self.stealth \ 209 | and not self.dormant \ 210 | and not self.immune 211 | 212 | @property 213 | def can_be_attacked(self): 214 | return not self.stealth \ 215 | and not self.immune \ 216 | and not self.dormant 217 | 218 | # 简单介绍一下卡费理论 219 | # 一点法力水晶 = 抽0.5张卡 = 造成1点伤害 220 | # = 2点攻击力 = 2点生命值 = 回复2点血 221 | # 一张卡自带一点水晶 222 | # 可以类比一下月火术, 奥术射击, 小精灵, 战斗法师等卡 223 | @property 224 | def heuristic_val(self): 225 | if self.health <= 0: 226 | return 0 227 | 228 | h_val = self.attack + self.health 229 | if self.divine_shield: 230 | h_val += self.attack 231 | if self.stealth: 232 | h_val += self.attack / 2 233 | if self.taunt: # 嘲讽不值钱 234 | h_val += self.health / 4 235 | if self.poisonous: 236 | h_val += self.health 237 | if self.divine_shield: 238 | h_val += 3 239 | if self.life_steal: 240 | h_val += self.attack / 2 + self.health / 4 241 | h_val += self.poisonous 242 | 243 | if self.zone == "HAND": 244 | if self.rush or self.attack: 245 | h_val += self.attack / 4 246 | 247 | h_val *= CRITICAL_MINION.get(self.card_id, 1) 248 | 249 | return h_val 250 | 251 | def get_damaged(self, damage): 252 | if damage <= 0: 253 | return False 254 | if self.divine_shield: 255 | self.divine_shield = False 256 | else: 257 | self.damage += damage 258 | if self.health <= 0: 259 | return True 260 | return False 261 | 262 | def get_heal(self, heal): 263 | if heal > self.damage: 264 | self.damage = 0 265 | else: 266 | self.damage -= heal 267 | 268 | def delta_h_after_damage(self, damage): 269 | temp_minion = copy.copy(self) 270 | temp_minion.get_damaged(damage) 271 | delta = self.heuristic_val - temp_minion.heuristic_val 272 | 273 | if self.is_mine: 274 | delta *= MY_DELTA_H_FACTOR 275 | else: 276 | delta *= OPPO_DELTA_H_FACTOR 277 | 278 | return delta 279 | 280 | def delta_h_after_heal(self, heal): 281 | temp_minion = copy.copy(self) 282 | temp_minion.get_heal(heal) 283 | return temp_minion.heuristic_val - self.heuristic_val 284 | 285 | 286 | class StrategyWeapon(StrategyEntity): 287 | def __init__(self, card_id, zone, zone_pos, 288 | current_cost, overload, is_mine, 289 | attack, durability, damage=0, windfury=0): 290 | super().__init__(card_id, zone, zone_pos, 291 | current_cost, overload, is_mine) 292 | self.attack = attack 293 | self.durability = durability 294 | self.damage = damage 295 | self.windfury = windfury 296 | 297 | def __str__(self): 298 | temp = f"{self.name} {self.attack}-{self.health}" \ 299 | f"({self.durability}) h_val:{self.heuristic_val}" 300 | if self.windfury: 301 | temp += " 风怒" 302 | return temp 303 | 304 | @property 305 | def cardtype(self): 306 | return CARD_WEAPON 307 | 308 | @property 309 | def health(self): 310 | return self.durability - self.damage 311 | 312 | @property 313 | def heuristic_val(self): 314 | return self.attack * self.health 315 | 316 | 317 | class StrategyHero(StrategyEntity): 318 | def __init__(self, card_id, zone, zone_pos, 319 | current_cost, overload, is_mine, 320 | max_health, damage=0, 321 | stealth=0, immune=0, 322 | not_targeted_by_spell=0, not_targeted_by_power=0, 323 | armor=0, attack=0, exhausted=1): 324 | super().__init__(card_id, zone, zone_pos, 325 | current_cost, overload, is_mine) 326 | self.max_health = max_health 327 | self.damage = damage 328 | self.stealth = stealth 329 | self.immune = immune 330 | self.not_targeted_by_spell = not_targeted_by_spell 331 | self.not_targeted_by_power = not_targeted_by_power 332 | self.armor = armor 333 | self.attack = attack 334 | self.exhausted = exhausted 335 | 336 | def __str__(self): 337 | temp = f"{self.name} {self.attack}-{self.health}" \ 338 | f"({self.max_health - self.damage}+{self.armor})" 339 | 340 | if self.can_attack: 341 | temp += " [能动]" 342 | else: 343 | temp += " [不能动]" 344 | 345 | if self.stealth: 346 | temp += " 潜行" 347 | if self.immune: 348 | temp += " 免疫" 349 | 350 | temp += f" h_val:{self.heuristic_val}" 351 | return temp 352 | 353 | @property 354 | def cardtype(self): 355 | return CARD_HERO 356 | 357 | @property 358 | def uni_index(self): 359 | if self.is_mine: 360 | return 9 361 | else: 362 | return 19 363 | 364 | @property 365 | def health(self): 366 | return self.max_health + self.armor - self.damage 367 | 368 | @property 369 | def heuristic_val(self): 370 | if self.health <= 0: 371 | return -10000 372 | if self.health <= 5: 373 | return self.health 374 | if self.health <= 10: 375 | return 5 + (self.health - 5) * 0.6 376 | if self.health <= 20: 377 | return 8 + (self.health - 10) * 0.4 378 | else: 379 | return 12 + (self.health - 20) * 0.3 380 | 381 | @property 382 | def can_attack(self): 383 | return self.attack > 0 and not self.exhausted 384 | 385 | @property 386 | def can_be_pointed_by_spell(self): 387 | return not self.stealth \ 388 | and not self.not_targeted_by_spell \ 389 | and not self.immune 390 | 391 | @property 392 | def can_be_pointed_by_hero_power(self): 393 | return not self.stealth \ 394 | and not self.not_targeted_by_power \ 395 | and not self.immune 396 | 397 | @property 398 | def can_be_pointed_by_minion(self): 399 | return not self.stealth \ 400 | and not self.immune 401 | 402 | @property 403 | def can_be_attacked(self): 404 | return not self.stealth and not self.immune 405 | 406 | def get_damaged(self, damage): 407 | if damage <= self.armor: 408 | self.armor -= damage 409 | else: 410 | last_damage = damage - self.armor 411 | self.armor = 0 412 | self.damage += last_damage 413 | 414 | def get_heal(self, heal): 415 | if heal >= self.damage: 416 | self.damage = 0 417 | else: 418 | self.damage -= heal 419 | 420 | def delta_h_after_damage(self, damage): 421 | temp_hero = copy.copy(self) 422 | temp_hero.get_damaged(damage) 423 | return self.heuristic_val - temp_hero.heuristic_val 424 | 425 | def delta_h_after_heal(self, heal): 426 | temp_hero = copy.copy(self) 427 | temp_hero.get_heal(heal) 428 | return temp_hero.heuristic_val - self.heuristic_val 429 | 430 | 431 | class StrategySpell(StrategyEntity): 432 | @property 433 | def cardtype(self): 434 | return CARD_SPELL 435 | 436 | 437 | class StrategyHeroPower(StrategyEntity): 438 | def __init__(self, card_id, zone, zone_pos, 439 | current_cost, overload, is_mine, 440 | exhausted): 441 | super().__init__(card_id, zone, zone_pos, 442 | current_cost, overload, is_mine) 443 | self.exhausted = exhausted 444 | 445 | @property 446 | def cardtype(self): 447 | return CARD_HERO_POWER 448 | 449 | @property 450 | def detail_hero_power(self): 451 | if self.name == "次级治疗术": 452 | return ID2CARD_DICT["LESSER_HEAL"] 453 | if self.name == "图腾召唤": 454 | return ID2CARD_DICT["TOTEMIC_CALL"] 455 | if self.name == "稳固射击": 456 | return ID2CARD_DICT["BALLISTA_SHOT"] 457 | return None 458 | -------------------------------------------------------------------------------- /FSM_action.py: -------------------------------------------------------------------------------- 1 | from ctypes.wintypes import PINT 2 | import random 3 | import sys 4 | import time 5 | 6 | import keyboard 7 | 8 | import requests 9 | 10 | import click 11 | import get_screen 12 | from strategy import StrategyState 13 | from log_state import * 14 | 15 | from datetime import datetime 16 | 17 | FSM_state = "" 18 | time_begin = 0.0 19 | game_count = 0 20 | win_count = 0 21 | quitting_flag = False 22 | log_state = LogState() 23 | log_iter = log_iter_func(HEARTHSTONE_POWER_LOG_PATH) 24 | choose_hero_count = 0 25 | 26 | 27 | def init(): 28 | global log_state, log_iter, choose_hero_count 29 | 30 | # 有时候炉石退出时python握着Power.log的读锁, 因而炉石无法 31 | # 删除Power.log. 而当炉石重启时, 炉石会从头开始写Power.log, 32 | # 但此时python会读入完整的Power.log, 并在原来的末尾等待新的写入. 那 33 | # 样的话python就一直读不到新的log. 状态机进而卡死在匹配状态(不 34 | # 知道对战已经开始) 35 | # 这里是试图在每次初始化文件句柄的时候删除已有的炉石日志. 如果要清空的 36 | # 日志是关于当前打开的炉石的, 那么炉石会持有此文件的写锁, 使脚本无法 37 | # 清空日志. 这使得脚本不会清空有意义的日志 38 | if os.path.exists(HEARTHSTONE_POWER_LOG_PATH): 39 | try: 40 | file_handle = open(HEARTHSTONE_POWER_LOG_PATH, "w") 41 | file_handle.seek(0) 42 | file_handle.truncate() 43 | info_print("Success to truncate Power.log") 44 | except OSError: 45 | warn_print("Fail to truncate Power.log, maybe someone is using it") 46 | else: 47 | info_print("Power.log does not exist") 48 | 49 | log_state = LogState() 50 | log_iter = log_iter_func(HEARTHSTONE_POWER_LOG_PATH) 51 | choose_hero_count = 0 52 | 53 | 54 | def update_log_state(): 55 | log_container = next(log_iter) 56 | if log_container.log_type == LOG_CONTAINER_ERROR: 57 | return False 58 | 59 | for log_line_container in log_container.message_list: 60 | ok = update_state(log_state, log_line_container) 61 | # if not ok: 62 | # return False 63 | 64 | if DEBUG_FILE_WRITE: 65 | with open("./log/game_state_snapshot.txt", "w", encoding="utf8") as f: 66 | f.write(str(log_state)) 67 | 68 | # 注意如果Power.log没有更新, 这个函数依然会返回. 应该考虑到game_state只是被初始化 69 | # 过而没有进一步更新的可能 70 | if log_state.game_entity_id == 0: 71 | return False 72 | 73 | return True 74 | 75 | 76 | def push_wx(sckey, desp=""): 77 | """ 78 | 推送消息到微信 79 | """ 80 | if sckey == '': 81 | print("[注意] 未提供sckey,不进行推送!") 82 | else: 83 | server_url = f"https://sc.ftqq.com/{sckey}.send" 84 | params = { 85 | "text": '小米运动 步数修改', 86 | "desp": desp 87 | } 88 | 89 | response = requests.get(server_url, params=params) 90 | json_data = response.json() 91 | 92 | if json_data['data']['errno'] == 0: 93 | print("{}, {}点,推送成功".format(datetime.now().date(), datetime.now().hour)) 94 | else: 95 | print("{}, {}点,推送失败,{},{}".format(datetime.now().date(), datetime.now().hour, json_data['data']['errno'], 96 | json_data['data']['errmsg'])) 97 | # print(f"[{datetime.now().date()}] 推送失败:{json_data['data']['errno']}({json_data['data']['errmsg']})") 98 | 99 | 100 | def system_exit(): 101 | global quitting_flag 102 | 103 | sckey = "" 104 | # 这个码是推送信息到微信的码,去sct.ftqq.com上绑定,不需要就不用管 105 | 106 | push = f"一共完成了{game_count}场对战, 赢了{win_count}场" 107 | 108 | try: 109 | push_wx(sckey, push) 110 | except: 111 | print("无法推送") 112 | 113 | sys_print(f"一共完成了{game_count}场对战, 赢了{win_count}场") 114 | print_info_close() 115 | 116 | quitting_flag = True 117 | 118 | sys.exit(0) 119 | 120 | 121 | def print_out(): 122 | global FSM_state 123 | global time_begin 124 | global game_count 125 | 126 | # sys_print("Enter State " + str(FSM_state)) 127 | 128 | if FSM_state == FSM_LEAVE_HS: 129 | warn_print("HearthStone not found! Try to go back to HS") 130 | 131 | if FSM_state == FSM_CHOOSING_CARD: 132 | game_count += 1 133 | # sys_print("The " + str(game_count) + " game begins") 134 | time_begin = time.time() 135 | 136 | if FSM_state == FSM_QUITTING_BATTLE: 137 | # sys_print("The " + str(game_count) + " game ends") 138 | time_now = time.time() 139 | if time_begin > 0: 140 | info_print("The last game last for : {} mins {} secs" 141 | .format(int((time_now - time_begin) // 60), 142 | int(time_now - time_begin) % 60)) 143 | 144 | return 145 | 146 | 147 | def ChoosingHeroAction(): 148 | global choose_hero_count 149 | 150 | print_out() 151 | 152 | # 有时脚本会卡在某个地方, 从而在FSM_Matching 153 | # 和FSM_CHOOSING_HERO之间反复横跳. 这时候要 154 | # 重启炉石 155 | # choose_hero_count会在每一次开始留牌时重置 156 | choose_hero_count += 1 157 | if choose_hero_count >= 20: 158 | return FSM_ERROR 159 | 160 | time.sleep(2) 161 | click.match_opponent() 162 | time.sleep(1) 163 | return FSM_MATCHING 164 | 165 | 166 | def MatchingAction(): 167 | print_out() 168 | loop_count = 0 169 | 170 | while True: 171 | if quitting_flag: 172 | sys.exit(0) 173 | 174 | time.sleep(STATE_CHECK_INTERVAL+random.random()+random.random()+random.random()) 175 | 176 | click.commit_error_report() 177 | 178 | ok = update_log_state() 179 | if ok: 180 | if not log_state.is_end: 181 | return FSM_CHOOSING_CARD 182 | 183 | curr_state = get_screen.get_state() 184 | if curr_state == FSM_CHOOSING_HERO: 185 | return FSM_CHOOSING_HERO 186 | 187 | loop_count += 1 188 | # print("寻找对手计时器") 189 | # print(loop_count) 190 | if loop_count >= 60: 191 | warn_print("Time out in Matching Opponent") 192 | return FSM_ERROR 193 | 194 | 195 | def ChoosingCardAction(): 196 | global choose_hero_count 197 | choose_hero_count = 0 198 | 199 | print_out() 200 | time.sleep(21) 201 | loop_count = 0 202 | has_print = 0 203 | 204 | while True: 205 | ok = update_log_state() 206 | 207 | if not ok: 208 | return FSM_ERROR 209 | if log_state.game_num_turns_in_play > 0: 210 | return FSM_BATTLING 211 | if log_state.is_end: 212 | return FSM_QUITTING_BATTLE 213 | 214 | strategy_state = StrategyState(log_state) 215 | hand_card_num = strategy_state.my_hand_card_num 216 | 217 | # 等待被替换的卡牌 ZONE=HAND 218 | # 注意后手时幸运币会作为第五张卡牌算在手牌里, 故只取前四张手牌 219 | # 但是后手时 hand_card_num 仍然是 5 220 | for my_hand_index, my_hand_card in \ 221 | enumerate(strategy_state.my_hand_cards[:4]): 222 | detail_card = my_hand_card.detail_card 223 | 224 | if detail_card is None: 225 | should_keep_in_hand = \ 226 | my_hand_card.current_cost <= REPLACE_COST_BAR 227 | else: 228 | should_keep_in_hand = \ 229 | detail_card.keep_in_hand(strategy_state, my_hand_index) 230 | 231 | # if not has_print: 232 | # debug_print(f"手牌-[{my_hand_index}]({my_hand_card.name})" 233 | # f"是否保留: {should_keep_in_hand}") 234 | 235 | if not should_keep_in_hand: 236 | click.replace_starting_card(my_hand_index, hand_card_num) 237 | 238 | has_print = 1 239 | 240 | click.commit_choose_card() 241 | 242 | loop_count += 1 243 | if loop_count >= 60: 244 | warn_print("Time out in Choosing Opponent") 245 | return FSM_ERROR 246 | time.sleep(STATE_CHECK_INTERVAL+random.random()+random.random()+random.random()+random.random()+random.random()) 247 | 248 | 249 | def Battling(): 250 | global win_count 251 | global log_state 252 | 253 | print_out() 254 | 255 | not_mine_count = 0 256 | mine_count = 0 257 | last_controller_is_me = False 258 | 259 | while True: 260 | if quitting_flag: 261 | sys.exit(0) 262 | 263 | ok = update_log_state() 264 | if not ok: 265 | return FSM_ERROR 266 | 267 | if log_state.is_end: 268 | if log_state.my_entity.query_tag("PLAYSTATE") == "WON": 269 | win_count += 1 270 | info_print("你赢得了这场对战") 271 | else: 272 | info_print("你输了") 273 | return FSM_QUITTING_BATTLE 274 | 275 | # 在对方回合等就行了 276 | if not log_state.is_my_turn: 277 | last_controller_is_me = False 278 | mine_count = 0 279 | 280 | not_mine_count += 1 281 | #太久了就发表情 282 | if not_mine_count == 60: 283 | if random.random() < EMOJ_RATIO: 284 | click.emoj() 285 | #太久了就发表情 286 | if not_mine_count == 100: 287 | if random.random() < 0.5: 288 | click.emoj() 289 | if not_mine_count >= 400: 290 | warn_print("Time out in Opponent's turn") 291 | return FSM_ERROR 292 | 293 | continue 294 | 295 | # 接下来考虑在我的回合的出牌逻辑 296 | 297 | # 如果是这个我的回合的第一次操作 298 | if not last_controller_is_me: 299 | time.sleep(4) 300 | # 在游戏的第一个我的回合, 发一个你好 301 | # game_num_turns_in_play在每一个回合开始时都会加一, 即 302 | # 后手放第一个回合这个数是2 303 | if log_state.game_num_turns_in_play <= 2: 304 | # click.emoj(0) 305 | if random.random() < 0.3: 306 | click.emoj(0) 307 | else: 308 | # 在之后每个回合开始时有概率发表情 309 | if random.random() < EMOJ_RATIO: 310 | click.emoj() 311 | 312 | last_controller_is_me = True 313 | not_mine_count = 0 314 | mine_count += 1 315 | 316 | if mine_count >= 20: 317 | if mine_count >= 40: 318 | return FSM_ERROR 319 | click.end_turn() 320 | click.commit_error_report() 321 | click.cancel_click() 322 | time.sleep(STATE_CHECK_INTERVAL+random.random()) 323 | 324 | # debug_print("-" * 60) 325 | strategy_state = StrategyState(log_state) 326 | # strategy_state.debug_print_out() 327 | 328 | # 考虑要不要出牌 329 | index, args = strategy_state.best_h_index_arg() 330 | 331 | # index == -1 代表使用技能, -2 代表不出牌 332 | if index != -2: 333 | strategy_state.use_best_entity(index, args) 334 | continue 335 | 336 | # 如果不出牌, 考虑随从怎么打架 337 | my_index, oppo_index = strategy_state.get_best_attack_target() 338 | 339 | # my_index == -1代表英雄攻击, -2 代表不攻击 340 | if my_index != -2: 341 | strategy_state.my_entity_attack_oppo(my_index, oppo_index) 342 | else: 343 | click.end_turn() 344 | time.sleep(STATE_CHECK_INTERVAL+random.random()+random.random()) 345 | 346 | 347 | def QuittingBattle(): 348 | print_out() 349 | 350 | time.sleep(5) 351 | 352 | loop_count = 0 353 | while True: 354 | if quitting_flag: 355 | sys.exit(0) 356 | 357 | state = get_screen.get_state() 358 | if state in [FSM_CHOOSING_HERO, FSM_LEAVE_HS]: 359 | return state 360 | click.cancel_click() 361 | click.test_click() 362 | click.commit_error_report() 363 | 364 | loop_count += 1 365 | if loop_count >= 15: 366 | return FSM_ERROR 367 | 368 | time.sleep(STATE_CHECK_INTERVAL+random.random()+random.random()+random.random()) 369 | 370 | 371 | def GoBackHSAction(): 372 | global FSM_state 373 | 374 | print_out() 375 | time.sleep(3) 376 | 377 | while not get_screen.test_hs_available(): 378 | if quitting_flag: 379 | sys.exit(0) 380 | click.enter_HS() 381 | time.sleep(10) 382 | 383 | # 有时候炉石进程会直接重写Power.log, 这时应该重新创建文件操作句柄 384 | init() 385 | 386 | return FSM_WAIT_MAIN_MENU 387 | 388 | 389 | def MainMenuAction(): 390 | print_out() 391 | 392 | time.sleep(3) 393 | 394 | while True: 395 | if quitting_flag: 396 | sys.exit(0) 397 | 398 | click.enter_battle_mode() 399 | time.sleep(5) 400 | 401 | state = get_screen.get_state() 402 | 403 | # 重新连接对战之类的 404 | if state == FSM_BATTLING: 405 | ok = update_log_state() 406 | if ok and log_state.available: 407 | return FSM_BATTLING 408 | if state == FSM_CHOOSING_HERO: 409 | return FSM_CHOOSING_HERO 410 | 411 | 412 | def WaitMainMenu(): 413 | print_out() 414 | wait_main_menu_count = 0 415 | while get_screen.get_state() != FSM_MAIN_MENU: 416 | click.enter_battle_mode() 417 | time.sleep(5) 418 | wait_main_menu_count += 1 419 | if wait_main_menu_count >= 5: 420 | break 421 | return FSM_MAIN_MENU 422 | 423 | 424 | def HandleErrorAction(): 425 | print_out() 426 | 427 | if not get_screen.test_hs_available(): 428 | return FSM_LEAVE_HS 429 | else: 430 | click.commit_error_report() 431 | click.click_setting() 432 | time.sleep(0.5) 433 | # 先尝试点认输 434 | click.left_click(960, 380) 435 | time.sleep(2) 436 | 437 | get_screen.terminate_HS() 438 | time.sleep(STATE_CHECK_INTERVAL+random.random()) 439 | 440 | return FSM_LEAVE_HS 441 | 442 | 443 | def FSM_dispatch(next_state): 444 | dispatch_dict = { 445 | FSM_LEAVE_HS: GoBackHSAction, 446 | FSM_MAIN_MENU: MainMenuAction, 447 | FSM_CHOOSING_HERO: ChoosingHeroAction, 448 | FSM_MATCHING: MatchingAction, 449 | FSM_CHOOSING_CARD: ChoosingCardAction, 450 | FSM_BATTLING: Battling, 451 | FSM_ERROR: HandleErrorAction, 452 | FSM_QUITTING_BATTLE: QuittingBattle, 453 | FSM_WAIT_MAIN_MENU: WaitMainMenu, 454 | } 455 | 456 | debug_print(f"当前状态为:+{next_state}") 457 | if next_state not in dispatch_dict: 458 | error_print("Unknown state!") 459 | sys.exit() 460 | else: 461 | return dispatch_dict[next_state]() 462 | 463 | 464 | def AutoHS_automata(): 465 | global FSM_state 466 | 467 | if get_screen.test_hs_available(): 468 | hs_hwnd = get_screen.get_HS_hwnd() 469 | get_screen.move_window_foreground(hs_hwnd) 470 | time.sleep(0.5+random.random()) 471 | 472 | while 1: 473 | if quitting_flag: 474 | sys.exit(0) 475 | if FSM_state == "": 476 | FSM_state = get_screen.get_state() 477 | FSM_state = FSM_dispatch(FSM_state) 478 | 479 | #if FSM_state=="Quitting Battle" and ((datetime.now().hour>=23) or (datetime.now().hour<=6)): 480 | if FSM_state=="Quitting Battle" and ((datetime.now().hour>=12) or (datetime.now().hour<=6)): 481 | print("555555555555555555555555555555555555555555555555555555555555555") 482 | print("555555555555555555555555555555555555555555555555555555555555555") 483 | print("555555555555555555555555555555555555555555555555555555555555555") 484 | print("555555555555555555555555555555555555555555555555555555555555555") 485 | print("555555555555555555555555555555555555555555555555555555555555555") 486 | system_exit() 487 | #sys.exit(0) 488 | 489 | #sys.exit(0) 490 | 491 | 492 | 493 | 494 | if __name__ == "__main__": 495 | keyboard.add_hotkey("ctrl+q", system_exit) 496 | 497 | init() 498 | -------------------------------------------------------------------------------- /log_state.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from log_op import * 4 | from json_op import * 5 | from strategy_entity import * 6 | from print_info import * 7 | import constants.constants 8 | 9 | MY_NAME = constants.constants.YOUR_NAME 10 | 11 | 12 | def check_name(): 13 | global MY_NAME 14 | if MY_NAME == "ChangeThis#54321": 15 | MY_NAME = input("请输入你的炉石用户名, 例子: \"为所欲为、异灵术#54321\" (不用输入引号!)\n").strip() 16 | 17 | 18 | class LogState: 19 | def __init__(self): 20 | self.game_entity_id = 0 21 | self.player_id_map_dict = {} 22 | self.my_name = "" 23 | self.oppo_name = "" 24 | self.my_player_id = "0" 25 | self.oppo_player_id = "0" 26 | self.entity_dict = {} 27 | self.current_update_id = 0 28 | 29 | def __str__(self): 30 | res = \ 31 | f"""GameState: 32 | game_entity_id: {self.game_entity_id} 33 | my_name: {self.my_name} 34 | oppo_name: {self.oppo_name} 35 | my_player_id: {self.my_player_id} 36 | oppo_player_id: {self.oppo_player_id} 37 | current_update_id: {self.current_update_id} 38 | entity_keys: {[list(self.entity_dict.keys())]} 39 | 40 | """ 41 | key_list = list(self.entity_dict.keys()) 42 | key_list.sort(key=int) 43 | 44 | for key in key_list: 45 | if key == self.game_entity_id: 46 | res += "GameState-" 47 | elif key == self.my_entity_id: 48 | res += "MyEntity-" 49 | elif key == self.oppo_entity_id: 50 | res += "OppoEntity-" 51 | res += f"[{str(key)}]\n" 52 | res += str(self.entity_dict[key]) 53 | res += "\n" 54 | 55 | return res 56 | 57 | @property 58 | def is_end(self): 59 | return self.game_state == "COMPLETE" 60 | 61 | @property 62 | def current_update_entity(self): 63 | return self.entity_dict[self.current_update_id] 64 | 65 | @property 66 | def game_entity(self): 67 | return self.entity_dict[self.game_entity_id] 68 | 69 | @property 70 | # 这不是my_player_id, 而是指代表我这个玩家的那个entity的index 71 | def my_entity_id(self): 72 | return self.player_id_map_dict.get(self.my_player_id, 0) 73 | 74 | @property 75 | def my_entity(self): 76 | return self.entity_dict[self.my_entity_id] 77 | 78 | @property 79 | def oppo_entity_id(self): 80 | return self.player_id_map_dict.get(self.oppo_player_id, 0) 81 | 82 | @property 83 | def oppo_entity(self): 84 | return self.entity_dict[self.oppo_entity_id] 85 | 86 | @property 87 | def is_my_turn(self): 88 | return self.my_entity.query_tag("CURRENT_PLAYER") == "1" 89 | 90 | @property 91 | def my_last_mana(self): 92 | return self.my_entity.query_tag("RESOURCES") - \ 93 | self.my_entity.query_tag("RESOURCES_USED") 94 | 95 | @property 96 | def game_step(self): 97 | return self.game_entity.query_tag("STEP") 98 | 99 | @property 100 | def game_state(self): 101 | return self.game_entity.query_tag("STATE") 102 | 103 | @property 104 | def game_num_turns_in_play(self): 105 | return int(self.game_entity.query_tag("NUM_TURNS_IN_PLAY")) 106 | 107 | @property 108 | def available(self): 109 | return self.game_entity_id != 0 110 | 111 | def flush(self): 112 | self.__init__() 113 | 114 | def add_entity(self, entity_id, entity): 115 | assert entity_id.isdigit() 116 | self.entity_dict[entity_id] = entity 117 | 118 | def set_game_entity(self, game_entity_id, game_entity): 119 | self.game_entity_id = game_entity_id 120 | self.add_entity(game_entity_id, game_entity) 121 | 122 | def fetch_game_entity(self): 123 | return self.entity_dict[self.game_entity_id] 124 | 125 | def add_player_entity(self, player_entity_id, player_id, player_entity): 126 | self.add_entity(player_entity_id, player_entity) 127 | self.player_id_map_dict[player_id] = player_entity_id 128 | 129 | def is_my_entity(self, entity): 130 | return entity.query_tag("CONTROLLER") == self.my_player_id 131 | 132 | 133 | class Entity: 134 | def __init__(self): 135 | self.tag_dict = {} 136 | 137 | def __str__(self): 138 | res = "" 139 | for key, value in self.tag_dict.items(): 140 | res += f"\t{key}: {value}\n" 141 | return res 142 | 143 | def set_tag(self, tag, val): 144 | self.tag_dict[tag] = val 145 | 146 | def query_tag(self, tag, default_val="0"): 147 | return self.tag_dict.get(tag, default_val) 148 | 149 | @property 150 | def cardtype(self): 151 | return self.query_tag("CARDTYPE") 152 | 153 | @property 154 | def zone(self): 155 | return self.query_tag("ZONE") 156 | 157 | 158 | class GameEntity(Entity): 159 | pass 160 | 161 | 162 | class PlayerEntity(Entity): 163 | pass 164 | 165 | 166 | class CardEntity(Entity): 167 | def __init__(self, card_id): 168 | super().__init__() 169 | self.card_id = card_id 170 | 171 | def __str__(self): 172 | return "cardID: " + self.card_id + "\n" + \ 173 | "name: " + self.name + "\n" + \ 174 | super().__str__() 175 | 176 | def generate_strategy_entity(self, log_state): 177 | if self.cardtype == "MINION": 178 | return StrategyMinion( 179 | card_id=self.card_id, 180 | zone=self.query_tag("ZONE"), 181 | zone_pos=int(self.query_tag("ZONE_POSITION")), 182 | current_cost=int(self.query_tag("TAG_LAST_KNOWN_COST_IN_HAND")), 183 | overload=int(self.query_tag("OVERLOAD")), 184 | is_mine=log_state.is_my_entity(self), 185 | attack=int(self.query_tag("ATK")), 186 | max_health=int(self.query_tag("HEALTH")), 187 | damage=int(self.query_tag("DAMAGE")), 188 | taunt=int(self.query_tag("TAUNT")), 189 | divine_shield=int(self.query_tag("DIVINE_SHIELD")), 190 | stealth=int(self.query_tag("STEALTH")), 191 | windfury=int(self.query_tag("WINDFURY")), 192 | poisonous=int(self.query_tag("POISONOUS")), 193 | freeze=int(self.query_tag("FREEZE")), 194 | battlecry=int(self.query_tag("BATTLECRY")), 195 | spell_power=int(self.query_tag("SPELLPOWER")), 196 | not_targeted_by_spell=int(self.query_tag("CANT_BE_TARGETED_BY_SPELLS")), 197 | not_targeted_by_power=int(self.query_tag("CANT_BE_TARGETED_BY_HERO_POWERS")), 198 | charge=int(self.query_tag("CHARGE")), 199 | rush=int(self.query_tag("RUSH")), 200 | attackable_by_rush=int(self.query_tag("ATTACKABLE_BY_RUSH")), 201 | frozen=int(self.query_tag("FROZEN")), 202 | dormant=int(self.query_tag("DORMANT")), 203 | untouchable=int(self.query_tag("UNTOUCHABLE")), 204 | immune=int(self.query_tag("IMMUNE")), 205 | # -1代表标签缺失, 有两种情况会产生-1: 断线重连; 卡刚从手牌中被打出来 206 | exhausted=int(self.query_tag("EXHAUSTED")), 207 | cant_attack=int(self.query_tag("CANT_ATTACK")), 208 | num_turns_in_play=int(self.query_tag("NUM_TURNS_IN_PLAY")), 209 | ) 210 | elif self.cardtype == "SPELL": 211 | return StrategySpell( 212 | card_id=self.card_id, 213 | zone=self.query_tag("ZONE"), 214 | zone_pos=int(self.query_tag("ZONE_POSITION")), 215 | current_cost=int(self.query_tag("TAG_LAST_KNOWN_COST_IN_HAND")), 216 | overload=int(self.query_tag("OVERLOAD")), 217 | is_mine=log_state.is_my_entity(self), 218 | ) 219 | elif self.cardtype == "WEAPON": 220 | return StrategyWeapon( 221 | card_id=self.card_id, 222 | zone=self.query_tag("ZONE"), 223 | zone_pos=int(self.query_tag("ZONE_POSITION")), 224 | current_cost=int(self.query_tag("TAG_LAST_KNOWN_COST_IN_HAND")), 225 | overload=int(self.query_tag("OVERLOAD")), 226 | is_mine=log_state.is_my_entity(self), 227 | attack=int(self.query_tag("ATK")), 228 | durability=int(self.query_tag("DURABILITY")), 229 | damage=int(self.query_tag("DAMAGE")), 230 | windfury=int(self.query_tag("WINDFURY")), 231 | ) 232 | elif self.cardtype == "HERO": 233 | return StrategyHero( 234 | card_id=self.card_id, 235 | zone=self.query_tag("ZONE"), 236 | zone_pos=int(self.query_tag("ZONE_POS")), 237 | current_cost=int(self.query_tag("TAG_LAST_KNOWN_COST_IN_HAND")), 238 | overload=int(self.query_tag("OVERLOAD")), 239 | is_mine=log_state.is_my_entity(self), 240 | max_health=int(self.query_tag("HEALTH")), 241 | damage=int(self.query_tag("DAMAGE")), 242 | stealth=int(self.query_tag("STEALTH")), 243 | immune=int(self.query_tag("IMMUNE")), 244 | not_targeted_by_spell=int(self.query_tag("CANT_BE_TARGETED_BY_SPELLS")), 245 | not_targeted_by_power=int(self.query_tag("CANT_BE_TARGETED_BY_HERO_POWERS")), 246 | armor=int(self.query_tag("ARMOR")), 247 | attack=int(self.query_tag("ATK")), 248 | exhausted=int(self.query_tag("EXHAUSTED")), 249 | ) 250 | elif self.cardtype == "HERO_POWER": 251 | return StrategyHeroPower( 252 | card_id=self.card_id, 253 | zone=self.query_tag("ZONE"), 254 | zone_pos=int(self.query_tag("ZONE_POS")), 255 | current_cost=int(self.query_tag("TAG_LAST_KNOWN_COST_IN_HAND")), 256 | overload=int(self.query_tag("OVERLOAD")), 257 | is_mine=log_state.is_my_entity(self), 258 | exhausted=int(self.query_tag("EXHAUSTED")), 259 | ) 260 | else: 261 | return None 262 | 263 | @property 264 | def name(self): 265 | return query_json_dict(self.card_id) 266 | 267 | def update_card_id(self, card_id): 268 | self.card_id = card_id 269 | 270 | 271 | def update_state(state, line_info_container): 272 | if line_info_container.line_type == LOG_LINE_CREATE_GAME: 273 | # sys_print("Read in new game and flush state") 274 | state.flush() 275 | 276 | if line_info_container.line_type == LOG_LINE_GAME_ENTITY: 277 | game_entity = GameEntity() 278 | game_entity_id = line_info_container.info_dict["entity"] 279 | 280 | state.current_update_id = game_entity_id 281 | state.add_entity(game_entity_id, game_entity) 282 | state.game_entity_id = game_entity_id 283 | 284 | if line_info_container.line_type == LOG_LINE_PLAYER_ENTITY: 285 | player_entity = PlayerEntity() 286 | player_entity_id = line_info_container.info_dict["entity"] 287 | player_id = line_info_container.info_dict["player"] 288 | 289 | state.current_update_id = player_entity_id 290 | state.add_player_entity(player_entity_id, player_id, player_entity) 291 | 292 | if line_info_container.line_type == LOG_LINE_FULL_ENTITY: 293 | card_id = line_info_container.info_dict["card"] 294 | card_entity_id = line_info_container.info_dict["entity"] 295 | card_entity = CardEntity(card_id) 296 | 297 | state.current_update_id = card_entity_id 298 | state.add_entity(card_entity_id, card_entity) 299 | 300 | if line_info_container.line_type == LOG_LINE_SHOW_ENTITY: 301 | card_id = line_info_container.info_dict["card"] 302 | card_entity_id = line_info_container.info_dict["entity"] 303 | 304 | card_entity = state.entity_dict[card_entity_id] 305 | card_entity.update_card_id(card_id) 306 | state.current_update_id = card_entity_id 307 | 308 | if line_info_container.line_type == LOG_LINE_CHANGE_ENTITY: 309 | card_id = line_info_container.info_dict["card"] 310 | card_entity_id = line_info_container.info_dict["entity"] 311 | 312 | card_entity = state.entity_dict[card_entity_id] 313 | card_entity.update_card_id(card_id) 314 | state.current_update_id = card_entity_id 315 | 316 | if line_info_container.line_type == LOG_LINE_TAG_CHANGE: 317 | entity_string = line_info_container.info_dict["entity"] 318 | 319 | # 情形一 "TAG_CHANGE Entity=GameEntity" 320 | if entity_string == "GameEntity": 321 | entity_id = state.game_entity_id 322 | 323 | # 情形二 "TAG_CHANGE Entity=Example#51234" 324 | elif not entity_string.isdigit(): 325 | # 关于为什么用 "in" 而非 "==", 因为我总是懒得输入后面的数字 326 | if MY_NAME in entity_string: 327 | entity_id = state.my_entity_id 328 | if entity_string != state.my_name: 329 | state.my_name = entity_string 330 | else: 331 | entity_id = state.oppo_entity_id 332 | if entity_string != state.oppo_name: 333 | state.oppo_name = entity_string 334 | 335 | assert int(entity_id) <= 3 336 | 337 | # 情形三 "TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=14 ...]" 338 | # 此时的EntityId已经被提取出来了 339 | else: 340 | entity_id = entity_string 341 | 342 | if entity_id not in state.entity_dict: 343 | warn_print(f"Invalid entity_id: {entity_id}") 344 | warn_print(f"Current line container: {line_info_container}") 345 | return False 346 | 347 | tag = line_info_container.info_dict["tag"] 348 | value = line_info_container.info_dict["value"] 349 | 350 | state.entity_dict[entity_id].set_tag(tag, value) 351 | 352 | if line_info_container.line_type == LOG_LINE_TAG: 353 | tag = line_info_container.info_dict["tag"] 354 | value = line_info_container.info_dict["value"] 355 | 356 | # 在对战的一开始的时候, 对手的任何牌对你都是不可见的. 357 | # 故而在日志中发现的第一个不是英雄, 不是英雄技能, 而且 358 | # 你知道它的 CardID 的 Entity 一定是你的牌. 利用这 359 | # 张牌确定双方的 PlayerID 360 | if state.my_player_id == "0": 361 | if tag == "CARDTYPE" \ 362 | and value not in ["HERO", "HERO_POWER", "PLAYER", "GAME"] \ 363 | and "CONTROLLER" in state.current_update_entity.tag_dict: 364 | state.my_player_id = state.current_update_entity.query_tag("CONTROLLER") 365 | # 双方PlayerID, 一个是1, 一个是2 366 | state.oppo_player_id = str(3 - int(state.my_player_id)) 367 | # debug_print(f"my_player_id: {state.my_player_id}") 368 | 369 | state.current_update_entity.set_tag(tag, value) 370 | 371 | if line_info_container.line_type == LOG_LINE_PLAYER_ID: 372 | player_id = line_info_container.info_dict["player"] 373 | player_name = line_info_container.info_dict["name"] 374 | 375 | # 我发现用这里的信息很不靠谱, 正常情况下的两个player_name 376 | # 应该对手的是"UNKNOWN HUMAN PLAYER", 你的是自己的用户名, 377 | # 但有时两个都是"UNKNOWN HUMAN PLAYER", 有时又都是已知. 378 | # 所以只拿来做校验 379 | 380 | # 下面这种情况明显是发生了错误. 一般会出现在在对战过程中关闭炉石 381 | # 再重新启动炉石. 此时在构建过程中看到的第一个确切的卡可能是对手 382 | # 场上的怪而非我自己的手牌, 进而误判 my_player_id 383 | if player_id == state.oppo_player_id and \ 384 | MY_NAME in player_name: 385 | warn_print("my_player_id may be wrong") 386 | state.my_player_id, state.oppo_player_id = \ 387 | state.oppo_player_id, state.my_player_id 388 | 389 | return True 390 | 391 | 392 | if __name__ == "__main__": 393 | log_iter = log_iter_func("./Power.log") 394 | log_container = next(log_iter) 395 | temp_state = LogState() 396 | 397 | for x in log_container.message_list: 398 | # print(x) 399 | update_state(temp_state, x) 400 | print(temp_state) 401 | -------------------------------------------------------------------------------- /strategy.py: -------------------------------------------------------------------------------- 1 | import click 2 | import keyboard 3 | import sys 4 | import random 5 | 6 | from card.basic_card import MinionNoPoint 7 | from log_state import * 8 | from log_op import * 9 | from strategy_entity import * 10 | 11 | 12 | class StrategyState: 13 | def __init__(self, log_state=None): 14 | self.oppo_minions = [] 15 | self.oppo_graveyard = [] 16 | self.my_minions = [] 17 | self.my_hand_cards = [] 18 | self.my_graveyard = [] 19 | 20 | self.my_hero = None 21 | self.my_hero_power = None 22 | self.can_use_power = False 23 | self.my_weapon = None 24 | self.oppo_hero = None 25 | self.oppo_hero_power = None 26 | self.oppo_weapon = None 27 | self.oppo_hand_card_num = 0 28 | 29 | self.my_total_mana = int(log_state.my_entity.query_tag("RESOURCES")) 30 | self.my_used_mana = int(log_state.my_entity.query_tag("RESOURCES_USED")) 31 | self.my_temp_mana = int(log_state.my_entity.query_tag("TEMP_RESOURCES")) 32 | 33 | for entity in log_state.entity_dict.values(): 34 | if entity.query_tag("ZONE") == "HAND": 35 | if log_state.is_my_entity(entity): 36 | hand_card = entity.generate_strategy_entity(log_state) 37 | self.my_hand_cards.append(hand_card) 38 | else: 39 | self.oppo_hand_card_num += 1 40 | 41 | elif entity.zone == "PLAY": 42 | if entity.cardtype == "MINION": 43 | minion = entity.generate_strategy_entity(log_state) 44 | if log_state.is_my_entity(entity): 45 | self.my_minions.append(minion) 46 | else: 47 | self.oppo_minions.append(minion) 48 | 49 | elif entity.cardtype == "HERO": 50 | hero = entity.generate_strategy_entity(log_state) 51 | if log_state.is_my_entity(entity): 52 | self.my_hero = hero 53 | else: 54 | self.oppo_hero = hero 55 | 56 | elif entity.cardtype == "HERO_POWER": 57 | hero_power = entity.generate_strategy_entity(log_state) 58 | if log_state.is_my_entity(entity): 59 | self.my_hero_power = hero_power 60 | else: 61 | self.oppo_hero_power = hero_power 62 | 63 | elif entity.cardtype == "WEAPON": 64 | weapon = entity.generate_strategy_entity(log_state) 65 | if log_state.is_my_entity(entity): 66 | self.my_weapon = weapon 67 | else: 68 | self.oppo_weapon = weapon 69 | 70 | elif entity.zone == "GRAVEYARD": 71 | if log_state.is_my_entity(entity): 72 | self.my_graveyard.append(entity) 73 | else: 74 | self.oppo_graveyard.append(entity) 75 | 76 | self.my_minions.sort(key=lambda temp: temp.zone_pos) 77 | self.oppo_minions.sort(key=lambda temp: temp.zone_pos) 78 | self.my_hand_cards.sort(key=lambda temp: temp.zone_pos) 79 | 80 | def debug_print_battlefield(self): 81 | if not DEBUG_PRINT: 82 | return 83 | 84 | debug_print("对手英雄:") 85 | debug_print(" " + str(self.oppo_hero)) 86 | debug_print(f"技能:") 87 | debug_print(" " + self.oppo_hero_power.name) 88 | if self.oppo_weapon: 89 | debug_print("头上有把武器:") 90 | debug_print(" " + str(self.oppo_weapon)) 91 | if self.oppo_minion_num > 0: 92 | debug_print(f"对手有{self.oppo_minion_num}个随从:") 93 | for minion in self.oppo_minions: 94 | debug_print(" " + str(minion)) 95 | else: 96 | debug_print(f"对手没有随从") 97 | debug_print(f"总卡费启发值: {self.oppo_heuristic_value}") 98 | 99 | debug_print() 100 | 101 | debug_print("我的英雄:") 102 | debug_print(" " + str(self.my_hero)) 103 | debug_print(f"技能:") 104 | debug_print(" " + self.my_hero_power.name) 105 | if self.my_weapon: 106 | debug_print("头上有把武器:") 107 | debug_print(" " + str(self.my_weapon)) 108 | if self.my_minion_num > 0: 109 | debug_print(f"我有{self.my_minion_num}个随从:") 110 | for minion in self.my_minions: 111 | debug_print(" " + str(minion)) 112 | else: 113 | debug_print("我没有随从") 114 | debug_print(f"总卡费启发值: {self.my_heuristic_value}") 115 | 116 | def debug_print_out(self): 117 | if not DEBUG_PRINT: 118 | return 119 | 120 | debug_print(f"对手墓地:") 121 | debug_print(" " + ", ".join([entity.name for entity in self.oppo_graveyard])) 122 | debug_print(f"对手有{self.oppo_hand_card_num}张手牌") 123 | 124 | self.debug_print_battlefield() 125 | debug_print() 126 | 127 | debug_print(f"水晶: {self.my_last_mana}/{self.my_total_mana}") 128 | debug_print(f"我有{self.my_hand_card_num}张手牌:") 129 | for hand_card in self.my_hand_cards: 130 | debug_print(f" [{hand_card.zone_pos}] {hand_card.name} " 131 | f"cost:{hand_card.current_cost}") 132 | debug_print(f"我的墓地:") 133 | debug_print(" " + ", ".join([entity.name for entity in self.my_graveyard])) 134 | 135 | @property 136 | def my_last_mana(self): 137 | return self.my_total_mana - self.my_used_mana + self.my_temp_mana 138 | 139 | @property 140 | def oppo_minion_num(self): 141 | return len(self.oppo_minions) 142 | 143 | @property 144 | def my_minion_num(self): 145 | return len(self.my_minions) 146 | 147 | @property 148 | def my_hand_card_num(self): 149 | return len(self.my_hand_cards) 150 | 151 | # 用卡费体系算启发值 152 | @property 153 | def oppo_heuristic_value(self): 154 | total_h_val = self.oppo_hero.heuristic_val 155 | if self.oppo_weapon: 156 | total_h_val += self.oppo_weapon.heuristic_val 157 | for minion in self.oppo_minions: 158 | total_h_val += minion.heuristic_val 159 | return total_h_val 160 | 161 | @property 162 | def my_heuristic_value(self): 163 | total_h_val = self.my_hero.heuristic_val 164 | if self.my_weapon: 165 | total_h_val += self.my_weapon.heuristic_val 166 | for minion in self.my_minions: 167 | total_h_val += minion.heuristic_val 168 | return total_h_val 169 | 170 | @property 171 | def heuristic_value(self): 172 | return round(self.my_heuristic_value - self.oppo_heuristic_value, 3) 173 | 174 | @property 175 | def min_cost(self): 176 | minium = 100 177 | for hand_card in self.my_hand_cards: 178 | minium = min(minium, hand_card.current_cost) 179 | return minium 180 | 181 | @property 182 | def my_total_spell_power(self): 183 | return sum([minion.spell_power for minion in self.my_minions]) 184 | 185 | @property 186 | def my_detail_hero_power(self): 187 | return self.my_hero_power.detail_hero_power 188 | 189 | @property 190 | def touchable_oppo_minions(self): 191 | ret = [] 192 | 193 | for oppo_minion in self.oppo_minions: 194 | if oppo_minion.taunt and oppo_minion.can_be_pointed_by_minion: 195 | ret.append(oppo_minion) 196 | 197 | if len(ret) == 0: 198 | for oppo_minion in self.oppo_minions: 199 | if oppo_minion.can_be_pointed_by_minion: 200 | ret.append(oppo_minion) 201 | 202 | return ret 203 | 204 | @property 205 | def oppo_has_taunt(self): 206 | for oppo_minion in self.oppo_minions: 207 | if oppo_minion.taunt and not oppo_minion.stealth: 208 | return True 209 | 210 | return False 211 | 212 | @property 213 | def my_total_attack(self): 214 | count = 0 215 | for my_minion in self.my_minions: 216 | if my_minion.can_beat_face: 217 | count += my_minion.attack 218 | 219 | if self.my_hero.can_attack: 220 | count += self.my_hero.attack 221 | 222 | return count 223 | 224 | def fetch_uni_entity(self, uni_index): 225 | if 0 <= uni_index < 7: 226 | return self.my_minions[uni_index] 227 | elif uni_index == 9: 228 | return self.my_hero 229 | elif 10 <= uni_index < 17: 230 | return self.oppo_minions[uni_index] 231 | elif uni_index == 19: 232 | return self.oppo_hero 233 | else: 234 | error_print(f"Get invalid uni_index: {uni_index}") 235 | sys.exit(-1) 236 | 237 | def fight_between(self, oppo_index, my_index): 238 | oppo_minion = self.oppo_minions[oppo_index] 239 | my_minion = self.my_minions[my_index] 240 | 241 | if oppo_minion.get_damaged(my_minion.attack): 242 | self.oppo_minions.pop(oppo_index) 243 | 244 | if my_minion.get_damaged(oppo_minion.attack): 245 | self.my_minions.pop(my_index) 246 | 247 | def random_distribute_damage(self, damage, oppo_index_list, my_index_list): 248 | if len(oppo_index_list) == len(my_index_list) == 0: 249 | return 250 | 251 | random_x = random.randint(0, len(oppo_index_list) + len(my_index_list) - 1) 252 | 253 | if random_x < len(oppo_index_list): 254 | oppo_index = oppo_index_list[random_x] 255 | minion = self.oppo_minions[oppo_index] 256 | if minion.get_damaged(damage): 257 | self.oppo_minions.pop(oppo_index) 258 | else: 259 | my_index = my_index_list[random_x - len(oppo_index_list)] 260 | minion = self.my_minions[my_index] 261 | if minion.get_damaged(damage): 262 | self.my_minions.pop(my_index) 263 | 264 | def get_best_attack_target(self): 265 | touchable_oppo_minions = self.touchable_oppo_minions 266 | has_taunt = self.oppo_has_taunt 267 | beat_face_win = self.my_total_attack >= self.oppo_hero.health 268 | 269 | max_delta_h_val = 0 270 | max_my_index = -2 271 | max_oppo_index = -2 272 | min_attack = 0 273 | 274 | # 枚举每一个己方随从 275 | for my_index, my_minion in enumerate(self.my_minions): 276 | if not my_minion.can_attack_minion: 277 | continue 278 | 279 | # 如果没有墙,自己又能打脸,应该试一试 280 | if not has_taunt \ 281 | and my_minion.can_beat_face \ 282 | and self.oppo_hero.can_be_pointed_by_minion: 283 | if beat_face_win: 284 | debug_print(f"攻击决策: [{my_index}]({my_minion.name})->" 285 | f"[-1]({self.oppo_hero.name}) " 286 | f"斩杀了") 287 | return my_index, -1 288 | 289 | tmp_delta_h = self.oppo_hero.delta_h_after_damage(my_minion.attack) 290 | 291 | debug_print(f"攻击决策: [{my_index}]({my_minion.name})->" 292 | f"[-1]({self.oppo_hero.name}) " 293 | f"delta_h_val:{tmp_delta_h}") 294 | 295 | if tmp_delta_h > max_delta_h_val: 296 | max_delta_h_val = tmp_delta_h 297 | max_my_index = my_index 298 | max_oppo_index = -1 299 | min_attack = 999 300 | 301 | for oppo_minion in touchable_oppo_minions: 302 | oppo_index = oppo_minion.zone_pos - 1 303 | 304 | tmp_delta_h = 0 305 | tmp_delta_h -= my_minion.delta_h_after_damage(oppo_minion.attack) 306 | tmp_delta_h += oppo_minion.delta_h_after_damage(my_minion.attack) 307 | 308 | debug_print(f"攻击决策:[{my_index}]({my_minion.name})->" 309 | f"[{oppo_index}]({oppo_minion.name}) " 310 | f"delta_h_val: {tmp_delta_h}") 311 | 312 | if tmp_delta_h > max_delta_h_val or \ 313 | tmp_delta_h == max_delta_h_val and my_minion.attack < min_attack: 314 | max_delta_h_val = tmp_delta_h 315 | max_my_index = my_index 316 | max_oppo_index = oppo_index 317 | min_attack = my_minion.attack 318 | 319 | # 试一试英雄攻击 320 | if self.my_hero.can_attack: 321 | if not has_taunt and self.oppo_hero.can_be_pointed_by_minion: 322 | if beat_face_win: 323 | debug_print(f"攻击决策: [-1]({self.my_hero.name})->" 324 | f"[-1]({self.oppo_hero.name}) " 325 | f"斩杀了") 326 | return -1, -1 327 | 328 | for oppo_minion in touchable_oppo_minions: 329 | oppo_index = oppo_minion.zone_pos - 1 330 | 331 | tmp_delta_h = 0 332 | tmp_delta_h += oppo_minion.delta_h_after_damage(self.my_hero.attack) 333 | tmp_delta_h -= self.my_hero.delta_h_after_damage(oppo_minion.attack) 334 | if self.my_weapon is not None: 335 | tmp_delta_h -= self.my_weapon.attack 336 | 337 | debug_print(f"攻击决策: [-1]({self.my_hero.name})->" 338 | f"[{oppo_index}]({oppo_minion.name}) " 339 | f"delta_h_val: {tmp_delta_h}") 340 | 341 | if tmp_delta_h >= max_delta_h_val: 342 | max_delta_h_val = tmp_delta_h 343 | max_my_index = -1 344 | max_oppo_index = oppo_index 345 | 346 | debug_print(f"最终决策: max_my_index: {max_my_index}, " 347 | f"max_oppo_index: {max_oppo_index}") 348 | 349 | return max_my_index, max_oppo_index 350 | 351 | def my_entity_attack_oppo(self, my_index, oppo_index): 352 | if my_index == -1: 353 | if oppo_index == -1: 354 | click.hero_beat_hero() 355 | else: 356 | click.hero_beat_minion(oppo_index, self.oppo_minion_num) 357 | else: 358 | if oppo_index == -1: 359 | click.minion_beat_hero(my_index, self.my_minion_num) 360 | else: 361 | click.minion_beat_minion(my_index, self.my_minion_num, 362 | oppo_index, self.oppo_minion_num) 363 | 364 | def copy_new_one(self): 365 | # TODO: 有必要deepcopy吗 366 | tmp = copy.deepcopy(self) 367 | for i in range(self.oppo_minion_num): 368 | tmp.oppo_minions[i] = copy.deepcopy(self.oppo_minions[i]) 369 | for i in range(self.my_minion_num): 370 | tmp.my_minions[i] = copy.deepcopy(self.my_minions[i]) 371 | for i in range(self.my_hand_card_num): 372 | tmp.my_hand_cards[i] = copy.deepcopy(self.my_hand_cards[i]) 373 | return tmp 374 | 375 | def best_h_index_arg(self): 376 | debug_print() 377 | best_delta_h = 0 378 | best_index = -2 379 | best_args = [] 380 | 381 | # 考虑使用手牌 382 | for hand_card_index, hand_card in enumerate(self.my_hand_cards): 383 | delta_h = 0 384 | args = [] 385 | 386 | if hand_card.current_cost > self.my_last_mana: 387 | debug_print(f"卡牌-[{hand_card_index}]({hand_card.name}) 跳过") 388 | continue 389 | 390 | detail_card = hand_card.detail_card 391 | if detail_card is None: 392 | if hand_card.cardtype == CARD_MINION and not hand_card.battlecry: 393 | delta_h, *args = MinionNoPoint.best_h_and_arg(self, hand_card_index) 394 | debug_print(f"卡牌-[{hand_card_index}]({hand_card.name}) " 395 | f"delta_h: {delta_h}, *args: {[]} (默认行为) ") 396 | else: 397 | debug_print(f"卡牌[{hand_card_index}]({hand_card.name})无法评判") 398 | else: 399 | delta_h, *args = detail_card.best_h_and_arg(self, hand_card_index) 400 | debug_print(f"卡牌-[{hand_card_index}]({hand_card.name}) " 401 | f"delta_h: {delta_h}, *args: {args} (手写行为)") 402 | 403 | if delta_h > best_delta_h: 404 | best_delta_h = delta_h 405 | best_index = hand_card_index 406 | best_args = args 407 | 408 | # 考虑使用英雄技能 409 | if self.my_last_mana >= 2 and \ 410 | self.my_detail_hero_power and \ 411 | not self.my_hero_power.exhausted: 412 | hero_power = self.my_detail_hero_power 413 | 414 | delta_h, *args = hero_power.best_h_and_arg(self, -1) 415 | 416 | debug_print(f"技能-[ ]({self.my_hero_power.name}) " 417 | f"delta_h: {delta_h} " 418 | f"*args: {args}") 419 | 420 | if delta_h > best_delta_h: 421 | best_index = -1 422 | best_args = args 423 | else: 424 | debug_print(f"技能-[ ]({self.my_hero_power.name}) 跳过") 425 | 426 | debug_print(f"决策结果: best_delta_h:{best_delta_h}, " 427 | f"best_index:{best_index}, best_args:{best_args}") 428 | debug_print() 429 | return best_index, best_args 430 | 431 | def use_best_entity(self, index, args): 432 | if index == -1: 433 | debug_print("将使用技能") 434 | hero_power = self.my_detail_hero_power 435 | hero_power.use_with_arg(self, -1, *args) 436 | else: 437 | self.use_card(index, *args) 438 | 439 | # 会返回这张卡的cost 440 | def use_card(self, index, *args): 441 | hand_card = self.my_hand_cards[index] 442 | detail_card = hand_card.detail_card 443 | debug_print(f"将使用卡牌[{index}] {hand_card.name}") 444 | 445 | if detail_card is None: 446 | MinionNoPoint.use_with_arg(self, index, *args) 447 | else: 448 | detail_card.use_with_arg(self, index, *args) 449 | 450 | self.my_hand_cards.pop(index) 451 | return hand_card.current_cost 452 | 453 | 454 | if __name__ == "__main__": 455 | keyboard.add_hotkey("ctrl+q", sys.exit) 456 | 457 | log_iter = log_iter_func(HEARTHSTONE_POWER_LOG_PATH) 458 | state = LogState() 459 | 460 | while True: 461 | log_container = next(log_iter) 462 | if log_container.length > 0: 463 | for x in log_container.message_list: 464 | update_state(state, x) 465 | strategy_state = StrategyState(state) 466 | strategy_state.debug_print_out() 467 | 468 | with open("game_state_snapshot.txt", "w", encoding="utf8") as f: 469 | f.write(str(state)) 470 | 471 | mine_index, oppo_index = strategy_state.get_best_attack_target() 472 | debug_print(f"我的决策是: mine_index: {mine_index}, oppo_index: {oppo_index}") 473 | 474 | if mine_index != -1: 475 | if oppo_index == -1: 476 | click.minion_beat_hero(mine_index, strategy_state.my_minion_num) 477 | else: 478 | click.minion_beat_minion(mine_index, strategy_state.my_minion_num, 479 | oppo_index, strategy_state.oppo_minion_num) 480 | --------------------------------------------------------------------------------