MENU

炉石兄弟 DIY 汇总

March 15, 2020 • Read: 10917 • 炉石兄弟

2021.3.27 新版智能SIM转移,CardDB_cardIDEnum.cs生成脚本

# encoding: utf-8
import requests
import re
import json
import os
from tqdm import tqdm

ClassName = {
    "DREAM": "梦境",
    "WHIZBANG": "威兹班"
}

TypeName = {
    "ENCHANTMENT": "附魔",
    "HERO_POWER": "英雄技能"
}

CardSetName = {
    "TB": "TB乱斗模式",
    "HERO_SKINS": "HERO英雄皮肤和技能",
    "THE_BARRENS": "BAR贫瘠之地的锤炼",
    'SCHOLOMANCE': "SCH通灵学园",
    'BASIC': "CORE基本",
    'BATTLEGROUNDS': "BGS酒馆战棋",
    'BOOMSDAY': "BOT砰砰计划",
    'BRM': "BRM黑石山的火焰",
    'BLACK_TEMPLE': "BT外域的灰烬",
    'DEMON_HUNTER_INITIATE': "BT恶魔猎手新兵",
    'YEAR_OF_THE_DRAGON': "DRG巨龙降临",
    "TGT": "AT冠军的试炼",
    'GANGS': "CFM龙争虎斗加基森",
    'CORE': "CORE基本",
    'CREDITS': "CREDITS暴雪制作人员",
    'EXPERT1': "EX经典",
    'HOF': "HOF荣誉室",
    'DALARAN': "DAL暗影崛起",
    'DARKMOON_FAIRE': "DMF疯狂的暗月马戏团",
    'DRAGONS': "DRG巨龙降临",
    'NAXX': "NAX纳克萨玛斯",
    'GILNEAS': "GIL女巫森林",
    'GVG': "GVG地精大战侏儒",
    'ICECROWN': "ICC冰封王座的骑士",
    'UNGORO': "UNG勇闯安戈洛",
    'LOOTAPALOOZA': "LOOT狗头人与地下世界",
    'KARA': "KAR卡拉赞之夜",
    'LOE': "LOE探险者协会",
    'OG': "OG上古之神的低语",
    'MISSIONS': "MISSIONS新手训练",
    'TROLL': "TRL拉斯塔哈的大乱斗",
    'TAVERNS_OF_TIME': "TOT时光酒馆",
    'ULDUM': "ULD奥丹姆奇兵",
    'VANILLA': "VAN经典模式"
}

file_list = requests.get("https://api.hearthstonejson.com/v1/").text
ver_list = re.findall("/v1/(\d+)/all/", file_list)
new_version = max(ver_list)
print(f"new_version: {new_version}")

print("loading global_json...")
global_json = requests.get('https://api.hearthstonejson.com/v1/strings/zhCN/GLOBAL.json').text
global_data = json.loads(global_json)
assert global_data is not None
for name in global_data:
    if "GLOBAL_CLASS_" in name:
        class_name = name.replace('GLOBAL_CLASS_', '')
        if class_name not in ClassName:
            ClassName[class_name] = global_data[name]
    if "GLOBAL_CARD_SET_" in name:
        card_set = name.replace('GLOBAL_CARD_SET_', '')
        if card_set not in CardSetName:
            CardSetName[card_set] = global_data[name]
    elif "GLOBAL_CARDTYPE_" in name:
        card_type = name.replace('GLOBAL_CARDTYPE_', '')
        if card_type not in TypeName:
            TypeName[card_type] = global_data[name]

print("loaded global_json successfully!")

print("loading card_data...")
cardJson_data = ""
cardJson_File = f'{new_version}.json'
if os.path.exists(cardJson_File):
    print("--file mode")
    with open(cardJson_File, "r", encoding='utf-8') as f:
        cardJson_data = f.read()
    assert cardJson_data != ""
else:
    cardJson_url = f'https://api.hearthstonejson.com/v1/{new_version}/all/cards.json'
    print(f"--online mode({cardJson_url})")
    cardJson_req = requests.get(cardJson_url, stream=True)
    cardJson_byte = b''
    pbar = tqdm(total=-1, unit='B', unit_scale=True)
    for chunk in cardJson_req.iter_content(chunk_size=1024):
        assert chunk != None
        cardJson_byte += chunk
        pbar.update(1024)
    pbar.close()
    cardJson_data = cardJson_byte.decode()
    assert cardJson_data != ""
    with open(cardJson_File, "w", encoding='utf-8') as f:
        f.write(cardJson_data)

cardData_temp = json.loads(cardJson_data)
assert cardData_temp is not None
print("loaded card_json successfully!")

cardData = {}
for c in cardData_temp:
    cardData[c['id']] = c

sim_path = os.path.join(os.getcwd(), "cards")
print("loading sim_data from", sim_path)
Sim_Context = []
Sim_CardID = []
Sim_text_idx = {}
Sim_id_idx = {}
for root, dirs, files in os.walk(sim_path):
    for file in files:
        card_id = file.replace('Sim_', '').replace('.cs', '')
        with open(os.path.join(root, file), "r", encoding='utf-8') as sim_file:
            sim_content = sim_file.read()
            if "public" not in sim_content:
                continue
            card_idx = len(Sim_Context)
            Sim_Context.append(sim_content)
            Sim_CardID.append(card_id)
            Sim_id_idx[card_id] = card_idx
            if card_id in cardData and 'text' in cardData[card_id]:
                Sim_text_idx[cardData[card_id]['text']['zhCN']] = card_idx
print("loaded " + str(len(Sim_Context)) + " old_sim_data successfully!")

enum_data = ""
if not os.path.exists('sim'):
    os.mkdir('sim')

print("Creating sim file and CardDB_cardIDEnum.cs")
for cardid, c in cardData.items():
    sim_data = ""
    basic = ""
    if 'type' in c and 'name' in c and c['type'] in TypeName:
        # create enum data
        type = TypeName[c['type']]
        name_cn = c['name']['zhCN']
        name = c['name']['enUS']
        card_set = c['set']
        cardtext_cn = ""
        cardtext = ""
        if 'cardClass' in c and 'cost' in c:
            if type == '法术':
                basic = f"{ClassName[c['cardClass']]} 费用:{c['cost']}"
            if type == '随从' and 'attack' in c and 'health' in c:
                basic = f"{ClassName[c['cardClass']]} 费用:{c['cost']} 攻击力:{c['attack']} 生命值:{c['health']}"
            if type == '武器' and 'attack' in c and 'durability' in c:
                basic = f"{ClassName[c['cardClass']]} 费用:{c['cost']} 攻击力:{c['attack']} 耐久度:{c['durability']}"
            if type == '英雄技能':
                basic = f"{ClassName[c['cardClass']]} 费用:{c['cost']}"
        enum_data += "/// <summary>\n"
        enum_data += f"/// <para>{type} {basic}</para>\n"
        enum_data += f"/// <para>{name}</para>\n"
        enum_data += f"/// <para>{name_cn}</para>\n"
        if 'text' in c:
            cardtext_cn = c['text']['zhCN'].replace('\n', ' ')
            cardtext = c['text']['enUS'].replace('\n', ' ')
            enum_data += f"/// <para>{cardtext}</para>\n"
            enum_data += f"/// <para>{cardtext_cn}</para>\n"
        enum_data += "/// </summary>\n"
        enum_data += f"{cardid} = {c['dbfId']},\n"
        # create sim data
        if type == '附魔':
            continue
        while True:
            if cardid in Sim_id_idx:
                sim_data = Sim_Context[Sim_id_idx[cardid]]
                break
            if 'text' in c and c['text']['zhCN'] in Sim_text_idx:
                idx = Sim_text_idx[c['text']['zhCN']]
                sim_id = Sim_CardID[idx]
                if cardid not in sim_id:
                    sim_data = Sim_Context[idx]
                    # fix
                    sim_data = sim_data.replace(f'class Sim_{sim_id}', f'class Sim_{cardid}')
                    break
            sim_data = "using System;\nusing System.Collections.Generic;\nusing System.Text;\n\n"
            sim_data += "namespace HREngine.Bots\n{\n"
            sim_data += f"\tclass Sim_{cardid} : SimTemplate //* {name_cn} {name}\n\t{{\n"
            if cardtext != "":
                sim_data += "\t\t//" + cardtext + "\n"
                sim_data += "\t\t//" + cardtext_cn + "\n"
            sim_data += "\t\t\n\t\t\n\t}\n}\n"
            break

        if card_set not in CardSetName:
            print("check CardSetName:", cardid)
            continue

        # write sim data
        sim_dir = f"sim\\{CardSetName[card_set]}"
        if not os.path.exists(sim_dir):
            os.mkdir(sim_dir)
        with open(sim_dir + "\\Sim_" + cardid + ".cs", 'w', encoding='utf-8') as sim:
            sim.write(sim_data)
print("Write sim_data to " + os.path.join(os.getcwd(), "sim") + " successfully!")
# write id_enum data
enum_path = os.path.join(os.getcwd(), "CardDB_cardIDEnum.cs")

enum_file_data = '''namespace HREngine.Bots
{
    partial class CardDB
    {
        public enum cardIDEnum
        {
            None,            
'''
enum_data = enum_data.split('\n')
for line in enum_data:
    enum_file_data += f"\t\t\t{line}\n"

enum_file_data += '''        }
    }
}
'''

with open(enum_path, "w", encoding="utf-8") as cardIDEnum:
    cardIDEnum.write(enum_file_data)
print("Write CardDB_cardIDEnum.cs to " + enum_path + " successfully!")
print("all finish")

为炉石兄弟增加祈求机制 3月15日

祈求机制的改法,我们是模仿着兄弟原有的青玉魔像(JadeGolem)的机制来修改,所以我们很多添加函数的位置都选择在JadeGolem旁边,话不多说,这里就直接贴代码了

我们在Hrtprozis.cs文件中
搜索代码

public int anzEnemyJadeGolem = 0;

在下面添加两行

public int OwnInvoke = 0;
public int EnemyInvoke = 0;

搜索代码

help.logg("cthunbonus: " + this.anzOgOwnCThunAngrBonus + " " + this.anzOgOwnCThunHpBonus + " " + this.anzOgOwnCThunTaunt);

在下面添加一行

help.logg("invoke: " + this.OwnInvoke + " " + this.EnemyInvoke);

搜索代码

public void updateJadeGolemsInfo(int anzOwnJG, int anzEmemyJG)
{
    anzOwnJadeGolem = anzOwnJG;
    anzEnemyJadeGolem = anzEmemyJG;
}

在下面添加我们的自定义函数updateInvokedInfo

public void updateInvokedInfo(int Owninvoke, int Ememyinvoke)
{
    this.OwnInvoke = Owninvoke;
    this.EnemyInvoke = Ememyinvoke;
}

然后修改silverfish_HB.cs文件

Hrtprozis.Instance.updateJadeGolemsInfo(GameState.Get().GetFriendlySidePlayer().GetTag(GAME_TAG.JADE_GOLEM), GameState.Get().GetOpposingSidePlayer().GetTag(GAME_TAG.JADE_GOLEM));

的下一行添加调用我们所写的的自定义函数updateInvokedInfo

Hrtprozis.Instance.updateInvokedInfo(GameState.Get().GetFriendlySidePlayer().GetTag((GAME_TAG)1366), GameState.Get().GetOpposingSidePlayer().GetTag((GAME_TAG)1366));

最后修改Playfield.cs文件

搜索代码

public int anzEnemyJadeGolem = 0;

在下面添加两行

public int OwnInvoke = 0;
public int EnemyInvoke = 0;

搜索代码

this.anzEnemyJadeGolem = prozis.anzEnemyJadeGolem;

在下面添加两行

this.OwnInvoke = prozis.OwnInvoke;
this.EnemyInvoke = prozis.EnemyInvoke;

搜索代码

this.anzEnemyJadeGolem = p.anzEnemyJadeGolem;

在下面添加两行

this.OwnInvoke = p.OwnInvoke;
this.EnemyInvoke = p.EnemyInvoke;

搜索代码

retval += this.anzOgOwnCThunAngrBonus + this.anzOwnJadeGolem + this.anzOwnElementalsLastTurn;

修改为

retval += this.anzOgOwnCThunAngrBonus + this.anzOwnJadeGolem + this.OwnInvoke + this.anzOwnElementalsLastTurn;

搜索函数

public CardDB.Card getNextJadeGolem(bool own)

在这个函数上面添加getGalakrondInvoke函数

public void getGalakrondInvoke(bool own)
{
    int pos = own ? this.ownMinions.Count : this.enemyMinions.Count;
    CardDB.Card DRG_238t14t3kid = CardDB.Instance.getCardDataFromID(CardDB.cardIDEnum.DRG_238t14t3);
    CardDB.Card DRG_238t12t2kid = CardDB.Instance.getCardDataFromID(CardDB.cardIDEnum.DRG_238t12t2);
    if (own) this.OwnInvoke++; else this.EnemyInvoke++;
    TAG_CLASS cls = own ? this.ownHeroStartClass : this.enemyHeroStartClass;
    switch(cls)
    {
        case TAG_CLASS.SHAMAN://萨满
            this.callKid(DRG_238t14t3kid, pos, own);
            break;
        case TAG_CLASS.WARLOCK://术士
            this.callKid(DRG_238t12t2kid, pos, own);
            this.callKid(DRG_238t12t2kid, pos + 1, own);
            break;
        case TAG_CLASS.WARRIOR://战士
            this.minionGetTempBuff(own ? this.ownHero : this.enemyHero, 3, 0);
            break;
        case TAG_CLASS.PRIEST://牧师
            this.drawACard(CardDB.cardIDEnum.None, own, true);
            break;
        case TAG_CLASS.ROGUE://盗贼
            this.drawACard(CardDB.cardIDEnum.DAL_741, own, true);//虚灵跟班
            break;
        default:
            Helpfunctions.Instance.ErrorLog("[提示] 未知的英雄进行祈求");
            break;
    }
    return;
}

还有调试相关修改,如果你不会调试,可以不进行修改
BoardTester.cs文件

搜索代码

int anzEnemyJadeGolem = 0;

在下面添加两行

int OwnInvoke = 0;
int EnemyInvoke = 0;

搜索

if (s.StartsWith("elementals: "))

在上面添加代码

if (s.StartsWith("invoke: "))
{
    String[] ss = s.Split(' ');
    OwnInvoke = Convert.ToInt32(ss[1]);
    EnemyInvoke = Convert.ToInt32(ss[2]);
}

搜索

Hrtprozis.Instance.updateJadeGolemsInfo(this.anzOwnJadeGolem, this.anzEnemyJadeGolem);

在下一行添加

Hrtprozis.Instance.updateInvokedInfo(this.OwnInvoke, this.EnemyInvoke);

到此修改结束,如果有卡要进行祈求则使用public void getGalakrondInvoke(bool own)函数
如有不懂的地方,或者我写的不明白的地方请评论留言。
具体的文件位置可以看我另一篇文章。

炉石兄弟切换模式功能修复 3月20日

炉石发布会结束之后,兄弟的切换模式功能坏了
调试之后发现,问题主要存在于Option枚举被更新

解决方案
在主程序中替换新版Option(Triton.Game.Mapping.Option)
然后把Triton.Bot.Logic.Bots.DefaultBot.DefaultBot中对应的Option的值也改过去,因为就修改枚举,调用的Option值不会变化。
要修改的两个Option分别是
IN_WILD_MODE = 199
IN_RANKED_PLAY_MODE = 160

两种解决办法
1.直接用DnSpy修改IL代码(注意:直接用DnSpy修改代码会编译不通过)
2.用ILSpy反编译之后将代码复制到DnSpy中(原因好像是DnSpy的内置ILSpy版本比较老)

附:新版Option(2020-03-18)

public enum Option
{
    INVALID,
    CLIENT_OPTIONS_VERSION,
    SOUND,
    MUSIC,
    CURSOR,
    HUD,
    STREAMING,
    SOUND_VOLUME,
    MUSIC_VOLUME,
    GFX_WIDTH,
    GFX_HEIGHT,
    GFX_FULLSCREEN,
    HAS_SEEN_NEW_CINEMATIC,
    GFX_QUALITY,
    FAKE_PACK_OPENING,
    FAKE_PACK_COUNT,
    HEALTHY_GAMING_DEBUG,
    LAST_SCENE_MODE,
    LOCALE,
    IDLE_KICKER,
    IDLE_KICK_TIME,
    BACKGROUND_SOUND,
    PREFERRED_REGION,
    FORCE_SHOW_IKS,
    PEGUI_DEBUG,
    NEARBY_PLAYERS,
    GFX_WIN_CAMERA_CLEAR,
    GFX_MSAA,
    GFX_FXAA,
    GFX_TARGET_FRAME_RATE,
    GFX_VSYNC,
    CARD_BACK,
    CARD_BACK2,
    LOCAL_TUTORIAL_PROGRESS,
    CONNECT_TO_AURORA,
    SEASON_END_THRESHOLD,
    RECONNECT,
    RECONNECT_TIMEOUT,
    RECONNECT_RETRY_TIME,
    CHANGED_CARDS_DATA,
    KELTHUZADTAUNTS,
    GFX_WIN_POSX,
    GFX_WIN_POSY,
    PREFERRED_CDN_INDEX,
    LAST_FAILED_DOP_VERSION,
    TOUCH_MODE,
    SHOWN_GFX_DEVICE_WARNING,
    INTRO,
    TUTORIAL_LOST_PROGRESS,
    ERROR_SCREEN,
    WELCOME_QUEST_VIEWS,
    IKS_LAST_DOWNLOAD_TIME,
    IKS_LAST_DOWNLOAD_RESPONSE,
    IKS_CACHE_AGE,
    CHEAT_HISTORY,
    PRELOAD_CARD_ASSETS,
    COLLECTION_PREMIUM_TYPE,
    DEV_TIMESCALE,
    IKS_LAST_SHOWN_AD,
    SEEN_PACK_PRODUCT_LIST,
    SHOW_STANDARD_ONLY,
    SHOW_SET_ROTATION_INTRO_VISUALS,
    SKIP_ALL_MULLIGANS,
    IS_TEMPORARY_ACCOUNT_CHEAT,
    TEMPORARY_ACCOUNT_DATA,
    DISALLOWED_CLOUD_STORAGE,
    CREATED_ACCOUNT,
    LAST_HEAL_UP_EVENT_DATE,
    PUSH_NOTIFICATION_STATUS,
    DBF_XML_LOADING,
    HAS_SHOWN_DEVICE_PERFORMANCE_WARNING,
    SCREENSHOT_DIRECTORY,
    SIMULATE_CELLULAR,
    ASSET_DOWNLOAD_ENABLED,
    UPDATE_STATE,
    NATIVE_UPDATE_STATE,
    ASK_UNKNOWN_APPS,
    LAUNCH_COUNT,
    IS_INSTALL_REPORTED,
    FIRST_INSTALL_TIME,
    UPDATED_CLIENT_VERSION,
    UPDATE_STOP_LEVEL,
    MAX_DOWNLOAD_SPEED,
    STREAMING_SPEED_IN_GAME,
    AUTOCONVERT_VIRTUAL_CURRENCY,
    STREAMER_MODE,
    LATEST_SEEN_SHOP_PRODUCT_LIST,
    LATEST_DISPLAYED_SHOP_PRODUCT_LIST,
    RANK_DEBUG,
    DEBUG_CURSOR,
    SERVER_OPTIONS_VERSION,
    PAGE_MOUSE_OVERS,
    COVER_MOUSE_OVERS,
    AI_MODE,
    TIP_PRACTICE_PROGRESS,
    TIP_PLAY_PROGRESS,
    TIP_FORGE_PROGRESS,
    LAST_PRECON_HERO_CHOSEN,
    LAST_CUSTOM_DECK_CHOSEN,
    SELECTED_ADVENTURE,
    SELECTED_ADVENTURE_MODE,
    LAST_SELECTED_STORE_BOOSTER_ID,
    LAST_SELECTED_STORE_ADVENTURE_ID,
    LAST_SELECTED_STORE_HERO_ID,
    LATEST_SEEN_TAVERNBRAWL_SEASON,
    LATEST_SEEN_TAVERNBRAWL_SEASON_CHALKBOARD,
    TIMES_SEEN_TAVERNBRAWL_CRAZY_RULES_QUOTE,
    SET_ROTATION_INTRO_PROGRESS,
    TIMES_MOUSED_OVER_SWITCH_FORMAT_BUTTON,
    RETURNING_PLAYER_BANNER_SEEN,
    LATEST_SEEN_TAVERNBRAWL_SESSION_LIMIT,
    LAST_TAVERN_JOINED,
    LATEST_SEEN_FIRESIDEBRAWL_SEASON,
    LATEST_SEEN_FIRESIDEBRAWL_SEASON_CHALKBOARD,
    LATEST_SEEN_SCHEDULED_DOUBLE_GOLD_VO,
    LATEST_SEEN_SCHEDULED_ALL_POPUPS_SHOWN_VO,
    LATEST_SEEN_SCHEDULED_ENTERED_ARENA_DRAFT,
    LATEST_SEEN_SCHEDULED_LOGIN_FLOW_COMPLETE,
    LATEST_SEEN_WELCOME_QUEST_DIALOG,
    LATEST_SEEN_CURRENCY_CHANGED_VERSION,
    LATEST_SEEN_ADVENTURE,
    LATEST_SEEN_SCHEDULED_WELCOME_QUEST_SHOWN_VO,
    LATEST_SEEN_SCHEDULED_GENERIC_REWARD_SHOWN_VO,
    LATEST_SEEN_SCHEDULED_ARENA_REWARD_SHOWN_VO,
    LAST_SELECTED_STORE_PACK_TYPE,
    WHIZBANG_POPUP_COUNTER,
    SET_ROTATION_INTRO_PROGRESS_NEW_PLAYER,
    HAS_CLICKED_TOURNAMENT,
    HAS_OPENED_BOOSTER,
    HAS_SEEN_TOURNAMENT,
    HAS_SEEN_COLLECTIONMANAGER,
    JUST_FINISHED_TUTORIAL,
    SHOW_ADVANCED_COLLECTIONMANAGER,
    HAS_SEEN_PRACTICE_TRAY,
    HAS_SEEN_HUB,
    HAS_FINISHED_A_DECK,
    HAS_SEEN_FORGE,
    HAS_SEEN_FORGE_HERO_CHOICE,
    HAS_SEEN_FORGE_CARD_CHOICE,
    HAS_SEEN_FORGE_CARD_CHOICE2,
    HAS_SEEN_FORGE_PLAY_MODE,
    HAS_SEEN_FORGE_1WIN,
    HAS_SEEN_FORGE_2LOSS,
    HAS_SEEN_FORGE_RETIRE,
    HAS_SEEN_MULLIGAN,
    HAS_SEEN_EXPERT_AI,
    HAS_SEEN_EXPERT_AI_UNLOCK,
    HAS_SEEN_DECK_HELPER,
    HAS_SEEN_PACK_OPENING,
    HAS_SEEN_PRACTICE_MODE,
    HAS_SEEN_CUSTOM_DECK_PICKER,
    HAS_SEEN_ALL_BASIC_CLASS_CARDS_COMPLETE,
    HAS_BEEN_NUDGED_TO_CM,
    HAS_ADDED_CARDS_TO_DECK,
    TIP_CRAFTING_UNLOCKED,
    HAS_PLAYED_EXPERT_AI,
    HAS_DISENCHANTED,
    HAS_SEEN_SHOW_ALL_CARDS_REMINDER,
    HAS_SEEN_CRAFTING_INSTRUCTION,
    HAS_CRAFTED,
    IN_RANKED_PLAY_MODE,
    HAS_SEEN_THE_COIN,
    HAS_SEEN_100g_REMINDER,
    HAS_SEEN_GOLD_QTY_INSTRUCTION,
    HAS_SEEN_LEVEL_3,
    HAS_LOST_IN_ARENA,
    HAS_RUN_OUT_OF_QUESTS,
    HAS_ACKED_ARENA_REWARDS,
    HAS_SEEN_STEALTH_TAUNTER,
    FRIENDS_LIST_REQUEST_SECTION_HIDE,
    FRIENDS_LIST_CURRENTGAME_SECTION_HIDE,
    FRIENDS_LIST_FRIEND_SECTION_HIDE,
    FRIENDS_LIST_NEARBYPLAYER_SECTION_HIDE,
    FRIENDS_LIST_RECRUIT_SECTION_HIDE,
    HAS_SEEN_HEROIC_WARNING,
    HAS_SEEN_NAXX,
    HAS_ENTERED_NAXX,
    HAS_SEEN_NAXX_CLASS_CHALLENGE,
    BUNDLE_JUST_PURCHASE_IN_HUB,
    HAS_PLAYED_NAXX,
    SPECTATOR_OPEN_JOIN,
    HAS_STARTED_A_DECK,
    HAS_SEEN_COLLECTIONMANAGER_AFTER_PRACTICE,
    HAS_SEEN_BRM,
    HAS_SEEN_LOE,
    HAS_CLICKED_MANA_TAB,
    HAS_SEEN_FORGE_MAX_WIN,
    DEPRECATED_HAS_HEARD_TGT_PACK_VO,
    HAS_SEEN_LOE_STAFF_DISAPPEAR,
    HAS_SEEN_LOE_STAFF_REAPPEAR,
    HAS_SEEN_UNLOCK_ALL_HEROES_TRANSITION,
    SKIP_DECK_TEMPLATE_PAGE_FOR_CLASS_FLAGS,
    HAS_SEEN_DECK_TEMPLATE_SCREEN,
    HAS_CLICKED_DECK_TEMPLATE_REPLACE,
    HAS_SEEN_DECK_TEMPLATE_GHOST_CARD,
    HAS_REMOVED_CARD_FROM_DECK,
    HAS_SEEN_DELETE_DECK_REMINDER,
    HAS_CLICKED_COLLECTION_BUTTON_FOR_NEW_CARD,
    HAS_CLICKED_COLLECTION_BUTTON_FOR_NEW_DECK,
    IN_WILD_MODE,
    HAS_SEEN_WILD_MODE_VO,
    HAS_SEEN_STANDARD_MODE_TUTORIAL,
    NEEDS_TO_MAKE_STANDARD_DECK,
    HAS_SEEN_INVALID_ROTATED_CARD,
    SHOW_SWITCH_TO_WILD_ON_PLAY_SCREEN,
    SHOW_SWITCH_TO_WILD_ON_CREATE_DECK,
    SHOW_WILD_DISCLAIMER_POPUP_ON_CREATE_DECK,
    HAS_SEEN_BASIC_DECK_WARNING,
    GLOW_COLLECTION_BUTTON_AFTER_SET_ROTATION,
    HAS_SEEN_SET_FILTER_TUTORIAL,
    HAS_SEEN_RAF,
    HAS_SEEN_RAF_RECRUIT_URL,
    HAS_SEEN_KARA,
    HAS_SEEN_HEROIC_BRAWL,
    SHOW_INNKEEPER_DECK_DIALOGUE,
    HAS_HEARD_RETURNING_PLAYER_WELCOME_BACK_VO,
    HAS_SEEN_RETURNING_PLAYER_INNKEEPER_CHALLENGE_INTRO,
    DEPRECATED_HAD_ITEM_THAT_WILL_BE_ROTATED_IN_UPCOMING_EVENT,
    SHOULD_AUTO_CHECK_IN_TO_FIRESIDE_GATHERINGS,
    HAS_CLICKED_FIRESIDE_BRAWL_BUTTON,
    HAS_CLICKED_FIRESIDE_GATHERINGS_BUTTON,
    HAS_INITIATED_FIRESIDE_GATHERING_SCAN,
    HAS_DISABLED_GOLDENS_THIS_DRAFT,
    HAS_SEEN_FREE_ARENA_WIN_DIALOG_THIS_DRAFT,
    HAS_SEEN_ICC,
    LATEST_SEEN_ARENA_SEASON_STARTING,
    LATEST_SEEN_ARENA_SEASON_ENDING,
    HAS_SEEN_LOOT,
    HAS_SEEN_LATEST_DUNGEON_RUN_COMPLETE,
    HAS_SEEN_LOOT_CHARACTER_SELECT_VO,
    HAS_SEEN_LOOT_WELCOME_BANNER_VO,
    HAS_SEEN_LOOT_BOSS_FLIP_1_VO,
    HAS_SEEN_LOOT_BOSS_FLIP_2_VO,
    HAS_SEEN_LOOT_BOSS_FLIP_3_VO,
    HAS_SEEN_LOOT_OFFER_TREASURE_1_VO,
    HAS_SEEN_LOOT_OFFER_LOOT_PACKS_1_VO,
    HAS_SEEN_LOOT_OFFER_LOOT_PACKS_2_VO,
    HAS_JUST_SEEN_LOOT_NO_TAKE_CANDLE_VO,
    HAS_SEEN_LOOT_IN_GAME_WIN_VO,
    HAS_SEEN_LOOT_IN_GAME_LOSE_VO,
    HAS_SEEN_LOOT_IN_GAME_MULLIGAN_1_VO,
    HAS_SEEN_LOOT_IN_GAME_MULLIGAN_2_VO,
    HAS_SEEN_LOOT_COMPLETE_ALL_CLASSES_VO,
    HAS_SEEN_LOOT_BOSS_HERO_POWER,
    HAS_SEEN_LOOT_IN_GAME_LOSE_2_VO,
    HAS_SEEN_RANK_REVAMP_END_OF_GAME_WINS_REQUIRED,
    HAS_SEEN_GIL_BONUS_CHALLENGE,
    HAS_SEEN_PLAYED_TESS,
    HAS_SEEN_PLAYED_DARIUS,
    HAS_SEEN_PLAYED_SHAW,
    HAS_SEEN_PLAYED_TOKI,
    HAS_SEEN_BATTLEGROUNDS_BOX_BUTTON
}

深入探究炉石兄弟发现、抉择机制(第一篇:完美修复龙蹄的灾难选择) 3月25日

这是深入探究炉石兄弟发现、抉择机制的第一篇:完美修复龙蹄的灾难选择
先来看看龙蹄这张卡的牌面描述

龙蹄.png

那这个灾难究竟是怎么样的呢?我在网络上找了一张实战的图

龙蹄效果图.png

可以发现这就是类似于抉择一样的选择卡牌之后使用对应的功能,而对应到兄弟中也应该是与发现、抉择有关的代码
接着看看折腾版中与发现、抉择机制相关的代码

await Coroutine.Sleep(555 + makeChoice());
switch (dirtychoice)
{
    case 0: TritonHs.ChooseOneClickMiddle(); break;
    case 1: TritonHs.ChooseOneClickLeft(); break;
    case 2: TritonHs.ChooseOneClickRight(); break;
}
dirtychoice = -1;

可以看到通过makeChoice()函数调用发现相关的逻辑。
接下来是通过一个switch来调用内部函数进行选择。
可以看到
case 0 是选中间的一张牌
case 1 是选最左边的一张牌
case 2 是选最右边的一张牌

至于为什么不是从左到右的从零开始依次递增呢?我们等下来说,现在先来看makeChoice()相关的代码

private int makeChoice()
{
    if (dirtychoice < 1)
    {
        ...
        if (sourceCard.GetEntity().HasReferencedTag(GAME_TAG.DISCOVER))
        {
            choiceMode = GAME_TAG.DISCOVER;
            dirtychoice = -1;
        }
        else if (sourceCard.GetEntity().HasReferencedTag(GAME_TAG.ADAPT))
        {
            choiceMode = GAME_TAG.ADAPT;
            dirtychoice = -1;
        }
        DateTime tmp = DateTime.Now;
        ...
        if (dirtychoice == 0) dirtychoice = 1;
        else if (dirtychoice == 1) dirtychoice = 0;
        int ttf = (int)(DateTime.Now - tmp).TotalMilliseconds;
        if (ttf < 3000) return (new Random().Next(ttf < 1300 ? 1300 - ttf : 0, 3100 - ttf));
    }
    else
    {
        Helpfunctions.Instance.logg("选择这张卡牌: " + dirtychoice);
        return (new Random().Next(1100, 3200));
    }
}

从这里就可以看出makeChoice()大致的逻辑,返回的数据是需要的延迟时间,而标记需要选择的卡牌是通过dirtychoice
如果是发现或者是进化,则进入第一个分支进行判定并且通过
但是第二个分支是干什么用的呢?我们现在还不得而知。
而且我们可以发现,在第一个分支,兄弟特意对 0 和 1 的标记进行了替换

if (dirtychoice == 0) dirtychoice = 1;
else if (dirtychoice == 1) dirtychoice = 0;

index = 0的卡牌本来是最左的一张,通过上面两行代码变成1之后,又通过外部的switch又重新变成了最左的一张
index = 1的卡牌本来是中间的一张,通过上面两行代码变成0之后,又通过外部的switch又重新变成了中间的一张
这里说起来可能有点绕,需要细细的回味一下
有一种错错得正的感觉,但是为什么要做这样的变换呢?为什么不直接返回从左到右为0 1 2 的顺序呢?我们先放一放,等下再解释这个问题。

经过分析兄弟的发现代码,现在我们可以知道:
当兄弟选择灾难的第三张牌的时候,兄弟会调用TritonHs.ChooseOneClickRight()直接选择了最右边的一张
而当龙蹄要选择灾难的第四张卡的时候,却不会进行任何操作,因为直接跳出了switch语句,表现就是不进行任何操作

public static void ChooseOneClickRight()
{
    List<Card> friendlyCards = ChoiceCardMgr.Get().GetFriendlyCards();
    Client.LeftClickAt(Client.CardInteractPoint(friendlyCards[friendlyCards.Count - 1]));
}

这段代码是我反编译早期未加壳的兄弟版本得到的。
这里我稍微解释一下
第一行代码是利用ChoiceCardMgr.Get().GetFriendlyCards()对选择卡牌进行读取。
第二行代码通过CardInteractPoint函数得到了卡牌的相应坐标位置,然后调用LeftClickAt进行点击。
friendlyCards.Count - 1又意味着,选择的永远都是最右边的一张牌,没错这与函数名的含义一致,对于兄弟来说这样写没有问题
但是这对于龙蹄来说,当龙蹄要使用第三张统御(使你的所有其他随从获得+2/+2。)的时候
却使用了最右边的那张牌毁灭(对所有其他随从造成5点伤害。)
这可真蠢,明明要靠统御来加大场面,却用了毁灭把自己的所有场面清掉了。
于是我决定修改代码,将原来的仅仅支持3张卡从而转变为支持N张卡。

我的思路是直接函数内的调用方法,直接对卡牌进行点击
并且我决定去掉把内部0转成1 把1转成0,到外部调用的时候又反逻辑的调用(012对应中左右)这样的诡异的操作
于是我直接把内部函数的

if (dirtychoice == 0) dirtychoice = 1;
else if (dirtychoice == 1) dirtychoice = 0;

直接删掉了,并且学着TritonHs.ChooseOneClickRight()函数来编写外部调用,来替换原来的switch语句

List<Card> friendlyCards = ChoiceCardMgr.Get().GetFriendlyCards();
Client.LeftClickAt(Client.CardInteractPoint(friendlyCards[dirty]));

本以为万事大吉,直到我拿兄弟来挂德鲁伊互投的时候又发现了问题:一旦使用抉择卡牌就报错
内部逻辑显示选择左边那张,兄弟却执行的是选择了右边这张
内部逻辑显示选择右边那张,兄弟就直接报错了
经过思考之后才发现了根本原因

if (dirtychoice < 1)
{
    ...
}
else
{
    Helpfunctions.Instance.logg("选择这张卡牌: " + dirtychoice);
    return (new Random().Next(1100, 3200));
}

是的,问题就在于前面我们一直没有注意的第二个分支,他所做的事情是用于抉择卡牌的延迟
但是这个分支我们又没有进行任何修改怎么会出现问题呢?
于是我抱着尝试的心态去输出了一下dirtychoice,发现果然和我想的一样。
抉择中的选择不是从 0 开始的,抉择返回的并不是0 1 2这样的dirtychoice,而是只有 1 2,没有0的情况,并且1代表左边,2代表右边
而按照我之前的改法,抉择的这个分支返回1的时候就对应发现机制的选择第二张卡,返回2的时候就对应发现机制选择第三张卡。但是抉择总共只有两张卡,自然就报错了。

由此我们尝试的解释一下上面的问题,为什么0是中间的一张牌,而1是最左的一张,为什么兄弟要在发现的分支做这个看似无用的变换
我们分成两种情况来考虑
1.发现
数据返回最左一张是0,中间是1,最右一张是2
但是通过转换,使得
最左一张:dirtychoice = 1
中间一张:dirtychoice = 0
2.抉择
数据返回最左一张是1,最右一张是2
未经过任何转换,返回之后dirtychoice不变

最后在外部switch调用,
dirtychoice = 0 是选中间的一张牌
dirtychoice = 1 是选最左边的一张牌
dirtychoice = 2 是选最右边的一张牌
发现机制转换之后在外部再次转换之后变的正常。
抉择机制,1的时候选择最左,2的时候选择最右,也变得正常。

于是通过这样一个巧妙的转换,将抉择从1开始的序号和发现从0开始的序号巧妙的兼容起来了。
清楚了兄弟原本的代码意思之后,我对外部的选择进行了重写,得到了下面的代码

public void ChooseOneClick(int dirty)
{
    if (dirty >= 0)
    {
        switch (dirtychoice)
        {
            case 0: TritonHs.ChooseOneClickMiddle(); break;
            case 1: TritonHs.ChooseOneClickLeft(); break;
            default:
                {
                    List<Card> friendlyCards = ChoiceCardMgr.Get().GetFriendlyCards();
                    if (friendlyCards.Count > dirty)
                        Client.LeftClickAt(Client.CardInteractPoint(friendlyCards[dirty]));
                    else
                        TritonHs.ChooseOneClickRight();//抉择
                    break;
                }
        }
    }
}

代码中对 0 和 1 的情况依旧进入原来的函数进行调用。
但是对其他情况却进行了特殊的处理,当dirtychoice不为 0 或 1 的时候
会进行一次判定,如果friendlyCards.Count > dirty则意味着这是通过发现机制的一次正常选择,反之可以证明这是抉择机制的调用,至于为什么?我上面已经说得很明白了,还没懂的自己去面壁哦。

当然,只是修改了这个之后,还是会发现兄弟还是不会智能的选择卡牌,但是如果强制让其选择第四张牌已经不会报错了。
这个只是我们修复炉石兄弟发现、抉择机制的一个小小的铺垫,实战中还需要进一步的修改。

敬请期待
第二篇:如何更完美的获取发现卡牌的来源(目前兄弟的抉择必须是由兄弟自己打出才能正常的选择,如果你在选牌的界面才打开兄弟则就会提示:没有获得卡牌数据,这对于想要追求完美的我来说不能忍!)
第三篇:如何添加智能选牌数据库和修复类型识别问题

炉石新机制:休眠,流放 4月11日

休眠:双方玩家都无法触及。(感谢吧主
流放:在手牌中的最左或最右侧使用时触发的额外效果。

最近有点忙,话不多说直接挂代码吧

Minion.cs中还有几处赋值需要新增,可以学习lifesteal进行增加

silverfish_HB.csgetMinions函数加入对dormant数据的读取

m.dormant = entitiy.GetTag(GAME_TAG.DORMANT);
if (m.dormant > 0)
{
    m.untouchable = true;
    bool flag = false;
    foreach (var en in entitiy.AttachedCards)
    {
        if (en.Id == "BT_737e")
        {
            m.dormant = en.GetTag(GAME_TAG.SCORE_VALUE_1) - en.GetTag(GAME_TAG.SCORE_VALUE_2);
            flag = true;
            break;
        }
    }
    if (!flag) Helpfunctions.Instance.ErrorLog("未能读取到休眠信息");
}

SimTemplate.cs中新增三个函数

public virtual void onOutcast(Playfield p, bool own)
{
    return;
}
public virtual void onOutcast(Playfield p, Minion m)
{
    return;
}
public virtual void onDormantEndsTrigger(Playfield p, Minion m)
{
    return;
}

这里解释一下,第一个onOutcast代表法术牌的流放事件
第二个onOutcast代表随从牌的流放事件,类似于战吼
onDormantEndsTrigger指代随从唤醒事件。

Playfield.cs中的

public void playACard(Handmanager.Handcard hc, Minion target, int position, int choice, int penality)

函数下搜索

c.sim_card.onCardPlay(this, true, target, choice);

在这一行之下添加

//position从1开始,可以自行测试
if (hc.position == 1 || hc.position == this.owncards.Count)
{
    c.sim_card.onOutcast(this, true);
}

搜索

public void placeAmobSomewhere(Handmanager.Handcard hc, int choice, int zonepos)

函数下搜索

m.handcard.card.sim_card.getBattlecryEffect(this, m, hc.target, choice);

在这一行之下添加

if (hc.position == 1 || hc.position == this.owncards.Count)
{
    m.handcard.card.sim_card.onOutcast(this, m);
}

搜索

public void triggerStartTurn(bool ownturn)

函数下搜索

m.updateReadyness();

在这一行之下添加

if (m.dormant > 0 && ownturn == m.own)
{
    m.dormant--;
    if (m.dormant == 0)
    {
        m.handcard.card.sim_card.onDormantEndsTrigger(this, m);
    }
}

至此添加结束,接下来是我写的几张SIM
Sim_BT_004.cs

namespace HREngine.Bots
{
    class Sim_BT_004 : SimTemplate //* 被禁锢的眼魔 Imprisoned Observer
    {
        //<b>Dormant</b> for 2 turns.When this awakens,deal 2 damage to allenemy minions.
        //<b>休眠</b>两回合。唤醒时,对所有敌方随从造成2点伤害。
        public override void onDormantEndsTrigger(Playfield p, Minion m)
        {
            p.allMinionOfASideGetDamage(!m.own, 2);
        }

    }
}

Sim_BT_009.cs

namespace HREngine.Bots
{
    class Sim_BT_009 : SimTemplate //* 被禁锢的阳鳃鱼人 Imprisoned Sungill
    {
        //<b>Dormant</b> for 2 turns.When this awakens, summon two 1/1Murlocs.
        //<b>休眠</b>两回合。唤醒时,召唤两个1/1的鱼人。
        CardDB.Card kid = CardDB.Instance.getCardDataFromID(CardDB.cardIDEnum.BT_009t);
        public override void onDormantEndsTrigger(Playfield p, Minion m)
        {
            p.callKid(kid, m.zonepos, m.own);
            p.callKid(kid, m.zonepos + 1, m.own);
        }

    }
}

Sim_BT_121.cs

namespace HREngine.Bots
{
    class Sim_BT_121 : SimTemplate //* 被禁锢的甘尔葛 Imprisoned Gan'arg
    {
        //<b>Dormant</b> for 2 turns.When this awakens,equip a 3/2 Axe.
        //<b>休眠</b>两回合。唤醒时,装备一把3/2的斧子。
        CardDB.Card weapon = CardDB.Instance.getCardDataFromID(CardDB.cardIDEnum.CS2_106);
        public override void onDormantEndsTrigger(Playfield p, Minion m)
        {
            p.equipWeapon(weapon, m.own);
        }

    }
}

Sim_BT_127.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace HREngine.Bots
{
    class Sim_BT_127 : SimTemplate //* 被禁锢的萨特 Imprisoned Satyr
    {
        //[x]<b>Dormant</b> for 2 turns.When this awakens, reducethe Cost of a random minionin your hand by (5).
        //<b>休眠</b>两回合。唤醒时,使你手牌中的随机一张随从牌的法力值消耗减少(5)点。
        public override void onDormantEndsTrigger(Playfield p, Minion m)
        {
            if (m.own)
            {
                foreach (Handmanager.Handcard hc in p.owncards)
                {
                    hc.manacost = Math.Max(0, hc.manacost - 5);
                }
            }
        }

    }
}

Sim_BT_211.cs

namespace HREngine.Bots
{
    class Sim_BT_211 : SimTemplate //* 被禁锢的魔喉 Imprisoned Felmaw
    {
        //[x]<b>Dormant</b> for 2 turns.When this awakens,  attack a random enemy.
        //<b>休眠</b>两回合。唤醒时,随机攻击一个敌人。
        public override void onDormantEndsTrigger(Playfield p, Minion m)
        {
            Minion sm = p.searchRandomMinion(m.own ? p.enemyMinions : p.ownMinions, searchmode.searchHighestAttack);
            if (sm == null) sm = (m.own) ? p.enemyHero : p.ownHero;
            p.minionAttacksMinion(m, sm);
        }

    }
}

Sim_BT_305.cs

namespace HREngine.Bots
{
    class Sim_BT_305 : SimTemplate //* 被禁锢的拾荒小鬼 Imprisoned Scrap Imp
    {
        //<b>Dormant</b> for 2 turns.When this awakens,give all minions in your hand +2/+2.
        //<b>休眠</b>两回合。唤醒时,使你手牌中的所有随从牌获得+2/+2。
        public override void onDormantEndsTrigger(Playfield p, Minion m)
        {
            if (m.own)
            {
                foreach (Handmanager.Handcard hc in p.owncards)
                {
                    hc.addattack += 2;
                    hc.addHp += 2;
                }
            }
        }

    }
}

Sim_BT_934.cs

namespace HREngine.Bots
{
    class Sim_BT_934 : SimTemplate //* 被禁锢的安塔恩 Imprisoned Antaen
    {
        //[x]<b>Dormant</b> for 2 turns.When this awakens, deal10 damage randomly splitamong all enemies.
        //<b>休眠</b>两回合。唤醒时,造成10点伤害,随机分配到所有敌人身上。
        public override void onDormantEndsTrigger(Playfield p, Minion m)
        {
            p.allCharsOfASideGetRandomDamage(!m.own, 10);
        }
    }
}

2020-05-16 记录对炉石兄弟的修复

今天刚到家,前几天就看到群里面闹得沸沸腾腾,大概是因为炉石使用mono2.0后而且导致了兄弟大面积不可用,本来以为是比较麻烦的问题,结果没想到二十几分钟就解决了,但是由于现在局势紧张,也确实不方便透露具体内容,所以我只能从从技术的角度大致的说一下解决办法

首先是由于更新Mono.dll导致的偏移更新。
mono.dll -> mono-2.0-bdwgc.dll
还有相应的偏移需要更新,但是这里发现有个偏移地址是找不着的,但是经过研究似乎兄弟也没用到这个地址。

只修复上面那个还会有问题,经过调试可以定位到报错的位置,利用CE里面的结构体分析,可以快速的得到新的结构体偏移大小。修改后,测试可用。

最后是自己的一点感想
不希望看到留言邮箱让我发一份文件
1.我希望对炉石兄弟更多的是一种技术上的研究而并非是当做一个工具,当然这个只是对于在我博客上学习的人来说的,对于大部分来说那肯定还是一个挂机刷金币的工具,就我自己而言研究炉石兄弟还是给自己带来蛮多的乐趣的,我希望大家也能体会到这种乐趣。
2.不要对大神产生依赖性,就像是大家期待的吧主修复,或者说贴吧里哪一位大神来修复,我认为等别人修复使用这肯定不是一个长久之计,除非你愿意掏钱到淘宝上买服务,只要是白嫖,那肯定要做好哪一天不能用的准备。与其等别人修复,还不如尝试着自己动手,我这里给出一个时间数据,我用19年10月左右的那个版本,从头到尾修复可用,差不多花费了三天的时间,这个时间比我想象中的要短了很多,我给出这个数据的目的是为了告诉大家,其实找到一个老版本修复一下并不是什么难事,起码没有想象中的那么难,现在贴吧里,各种博客里也充满了兄弟的资料,现在研究比起早期研究兄弟的人简单太多了,只要你肯花心思花时间去学习。由于一些个人原因,接下来的两三个月里,我对兄弟的研究肯定要停止一段时间了。
3.这个博客的不是为了炉石兄弟而开设的,是作为一个个人博客的形象来呈现的,之所以现在的内容都关于炉石兄弟,因为这确实是我前段时间花了很大心思研究的东西,而之后我会分享自己其他方面的编程内容,炉石兄弟作为一个让自己入门C#的工具,其实到这里已经完成了我当时接触它时的使命了。

分享几个自用的用于整理卡牌数据的Python脚本 6月13日

修复换行问题,详情请见评论区

生成卡牌相关文件

import html
import os
import shutil
#输入:C:\\hs\\sim\\ 旧版的Sim文件
#输入:H:\CardDefs.xml 卡牌数据文件

#输出:C:\hs\CardDB_getSimCard.txt 生成的getSimCard文件
#输出:C:\hs\CardDB_cardIDEnum.txt 生成的cardIDEnum文件
#输出:C:\\hs\\newsim\\ 新版的Sim文件,保留旧版的Sim
TAG_CARDTYPE = {
        0: "INVALID",
        1: "GAME",
        2: "PLAYER",
        3: "英雄",
        4: "随从",
        5: "法术",
        6: "附魔",
        7: "武器",
        8: "ITEM",
        9: "TOKEN",
        10: "英雄技能",
        11: "BLANK",
        12: "GAME_MODE_BUTTON",
        22: "MOVE_MINION_HOVER_TARGET"
    }

TAG_CLASS = {
    0: "INVALID",
    1: "巫妖王",
    2: "德鲁伊",
    3: "猎人",
    4: "法师",
    5: "圣骑士",
    6: "潜行者",
    7: "牧师",
    8: "萨满祭司",
    9: "术士",
    10: "战士",
    11: "梦境",
    12: "中立",
    13: "威兹班",
    14: "恶魔猎手"
}
TAG_CARD_SET = {
    0:"INVALID",
    1:"TEST_TEMPORARY",
    2:"0001CORE基本",
    3:"0002EX经典",
    4:"0003HOF荣誉室",
    5:"MISSIONS新手训练",
    6:"DEMO",
    7:"NONE",
    8:"CHEAT",
    9:"BLANK",
    10:"DEBUG_SP",
    11:"PROMO",
    12:"0012NAX纳克萨玛斯",
    13:"0013GVG地精大战侏儒",
    14:"0014BRM黑石山的火焰",
    15:"0015AT冠军的试炼",
    16:"CREDITS暴雪制作人员",
    17:"0017英雄皮肤和技能",
    18:"0018TB乱斗模式",
    19:"SLUSH",
    20:"0020LOE探险者协会",
    21:"0021OG上古之神的低语",
    22:"OG_RESERVE",
    23:"0023KAR卡拉赞之夜",
    24:"KARA_RESERVE",
    25:"0025CFM龙争虎斗加基森",
    26:"GANGS_RESERVE",
    27:"0027UNG勇闯安戈洛",
    1001:"1001ICC冰封王座的骑士",
    1004:"1004LOOT狗头人与地下世界",
    1125:"1125GIL女巫森林",
    1127:"1127BOT砰砰计划",
    1129:"1129TRL拉斯塔哈的大乱斗",
    1130:"1130DAL暗影崛起",
    1158:"1158ULD奥丹姆奇兵" ,
    1347:"1347DRG巨龙降临",
    1439:"1439WE狂野限时回归" ,
    1403:"1403YOD迦拉克隆的觉醒",
    1453:"BATTLEGROUNDS" ,
    1414:"1414BT外域的灰烬" ,
    1463:"1463BT恶魔猎手新兵",
}

fs = open(r"C:\hs\CardDB_getSimCard.txt", 'w', encoding='utf-8')
fb = open(r"C:\hs\CardDB_cardIDEnum.txt", 'w', encoding='utf-8')
cardname = ""
cardnamecn = ""
cardtext = ""
cardtextcn = ""
cardID = ""
enumID = 0
numberID = 0
CARDTYPE = 0
CLASS = 0
CARD_SET = 0
COST = 0
ATK = 0
HEALTH = 0
DURABILITY = 0
with open(r"H:\CardDefs.xml", 'r', encoding='utf-8') as f:
    while 1:
        line = f.readline()
        if not line:
            break
        if r"</Entity>" in line:
            #BATTLEGROUNDS TB
            if CARD_SET != 1453 and CARD_SET != 1143:
                fb.write("/// <summary>\n")
                other = ""
                print(cardID)
                if TAG_CARDTYPE[CARDTYPE] == "法术" :
                    other += " " + TAG_CLASS[CLASS] + " 费用:" + str(COST)
                if TAG_CARDTYPE[CARDTYPE] == "随从" :
                    other += " " + TAG_CLASS[CLASS] + " 费用:" + str(COST) + " 攻击力:" + str(ATK) + " 生命值:" + str(HEALTH)
                if TAG_CARDTYPE[CARDTYPE] == "武器":
                    other += " " + TAG_CLASS[CLASS] + " 费用:" + str(COST) + " 攻击力:" + str(ATK) + " 耐久度:" + str(DURABILITY)
                if TAG_CARDTYPE[CARDTYPE] == "英雄技能":
                    other += " " + TAG_CLASS[CLASS] + " 费用:"
                fb.write("/// <para>"+ TAG_CARDTYPE[CARDTYPE] + other +"</para>\n")
                fb.write("/// <para>"+ cardname +"</para>\n")
                fb.write("/// <para>"+ cardnamecn +"</para>\n")
                fb.write("/// <para>"+ cardtext +"</para>\n")
                fb.write("/// <para>"+ cardtextcn +"</para>\n")
                fb.write("/// </summary>\n")
                fb.write(cardID + " = " + str(numberID) + ",\n")
                if TAG_CARDTYPE[CARDTYPE] != "附魔":
                    fs.write("case cardIDEnum." + cardID + ": return new Sim_" + cardID + "();\n")
                    directory = "C:\\hs\\newsim\\" + TAG_CARD_SET[CARD_SET]
                    if not os.path.exists(directory):
                        os.mkdir(directory)
                    if os.path.exists("C:\\hs\\sim\\Sim_" + cardID + ".cs"):
                        shutil.move("C:\\hs\\sim\\Sim_" + cardID + ".cs", directory + "\\Sim_" + cardID + ".cs")
                    else:
                        with open(directory + "\\Sim_" + cardID + ".cs", 'w', encoding='utf-8') as sim:
                            sim.write("using System;\nusing System.Collections.Generic;\nusing System.Text;\n\n");
                            sim.write("namespace HREngine.Bots\n{\n");
                            sim.write("\tclass Sim_" + cardID +" : SimTemplate //* " + cardnamecn + " " + cardname + "\n\t{\n")
                            sim.write("\t\t//" + cardtext + "\n")
                            sim.write("\t\t//" + cardtextcn + "\n")
                            sim.write("\t\t\n\t\t\n\t}\n}\n")

                #print(cardname, cardnamecn, cardID, TAG_CARDTYPE[CARDTYPE], TAG_CLASS[CLASS], cardtext, cardtextcn)
            cardname = ""
            cardnamecn = ""
            cardtext = ""
            cardtextcn = ""
            cardID = ""
            enumID = 0
            numberID = 0
            CARDTYPE = 0
            CLASS = 0
            CARD_SET = 0
            COST = 0
            ATK = 0
            HEALTH = 0
            DURABILITY = 0

        if "<Entity CardID=\"" in line:
            index1 = line.find("<Entity CardID=\"")
            index2 = line.find("\"", index1 + 16)
            if index1 == -1 or index2 == -1:
                print(line)
                exit(0)
            cardID = line[index1 + 16: index2]
            index1 = line.find("ID=\"", index2)
            index2 = line.find("\"", index1 + 4)
            if index1 == -1 or index2 == -1:
                print(line)
                exit(0)
            numberID = line[index1 + 4: index2]

        if "<Tag enumID=\"" in line:
            enumIDl = line.find("<Tag enumID=\"")
            enumIDr = line.find("\"", enumIDl + 13)
            if enumIDl == -1 or enumIDr == -1:
                print(line)
                exit(0)
            enumID = int(line[enumIDl + 13: enumIDr])
            valuel = line.find("value=\"")
            valuer = line.find("\"", valuel + 7)
            if valuel == -1 or valuer == -1:
                continue
            value = int(line[valuel + 7: valuer])
            #CARDTYPE
            if enumID == 202:
                CARDTYPE = value
            #CLASS
            if enumID == 199:
                CLASS = value
            #CARD_SET
            if enumID == 183:
                CARD_SET = value
            #COST
            if enumID == 48:
                COST = value
            #HEALTH
            if enumID == 45:
                HEALTH = value
            #ATK
            if enumID == 47:
                ATK = value
            #DURABILITY
            if enumID == 187:
                DURABILITY = value
        if "<enUS>" in line:
            if enumID != 185 and enumID != 184:
                continue
            index1 = line.find("<enUS>")
            index2 = line.find("</enUS>", index1 + 6)
            text = ""
            while (index2 == -1):
                text += line[index1 + 6:-1]
                line = f.readline()
                index2 = line.find("</enUS>")
                index1 = -6
            text += line[index1 + 6: index2]

            if index1 == -1 or index2 == -1:
                print("错误:" + line)
                exit(0)
            if enumID == 185:
                cardname = html.unescape(text)
            if enumID == 184:
                cardtext = html.unescape(text)

        if "<zhCN>" in line:
            if enumID != 185 and enumID != 184:
                continue
            index1 = line.find("<zhCN>")
            index2 = line.find("</zhCN>", index1 + 6)

            text = ""
            while (index2 == -1):
                text += line[index1 + 6:-1]
                line = f.readline()
                index2 = line.find("</zhCN>")
                index1 = -6
            text += line[index1 + 6: index2]

            if index1 == -1 or index2 == -1:
                print("错误:" + line)
                exit(0)
            if enumID == 185:
                cardnamecn = html.unescape(text)
            if enumID == 184:
                cardtextcn = html.unescape(text)

转移PlayRequirement到新的CardDefs文件

#输入:H:\CardDefsold.xml
#输出:H:\CardDefs.xml

import html
playreq = {}
playname = {}
with open(r"H:\CardDefsold.xml", 'r', encoding='utf-8') as f:
    for line in f:
        if r"</Entity>" in line:
            if cardID in playreq:
                playname[cardnamecn] = playreq[cardID]
            cardID = ""
            enumID = 0
            cardnamecn = ""
        if "<Entity CardID=\"" in line:
            index1 = line.find("<Entity CardID=\"")
            index2 = line.find("\"", index1 + 16)
            if index1 == -1 or index2 == -1:
                print(line)
                exit(0)
            cardID = line[index1 + 16: index2]

        if "<PlayRequirement" in line:
            if cardID not in playreq:
                playreq[cardID] = ""
            playreq[cardID] += line

        if "<Tag enumID=\"" in line:
            enumIDl = line.find("<Tag enumID=\"")
            enumIDr = line.find("\"", enumIDl + 13)
            if enumIDl == -1 or enumIDr == -1:
                print(line)
                exit(0)
            enumID = int(line[enumIDl + 13: enumIDr])
        if "<zhCN>" in line:
            if enumID != 185 and enumID != 184:
                continue
            index1 = line.find("<zhCN>")
            index2 = line.find("</zhCN>", index1 + 6)
            text = ""
            while (index2 == -1):
                text += line[index1 + 6:-1]
                line = f.readline()
                index2 = line.find("</zhCN>")
                index1 = -6
            text += line[index1 + 6: index2]
            if index1 == -1 or index2 == -1:
                print("错误:" + line)
                exit(0)
            if enumID == 185:
                cardnamecn = html.unescape(text)

#for key in playreq:
    #print(key)
    #print(playreq[key])
with open(r"H:\CardDefscopy.xml", 'w', encoding='utf-8') as fc:
    with open(r"H:\CardDefs.xml", 'r', encoding='utf-8') as f:
        for line in f:
            if r"</Entity>" in line:
                if cardID in playreq:
                    fc.write(playreq[cardID])
                else:
                    if cardnamecn in playname and CARDTYPE != 6 and CARDTYPE != 3:
                        print(cardID, cardnamecn)
                        print(playname[cardnamecn])
                        fc.write(playname[cardnamecn])
                cardID = ""
            if "<Entity CardID=\"" in line:
                index1 = line.find("<Entity CardID=\"")
                index2 = line.find("\"", index1 + 16)
                if index1 == -1 or index2 == -1:
                    print(line)
                    exit(0)
                cardID = line[index1 + 16: index2]
            fc.write(line)
            if "<Tag enumID=\"" in line:
                enumIDl = line.find("<Tag enumID=\"")
                enumIDr = line.find("\"", enumIDl + 13)
                if enumIDl == -1 or enumIDr == -1:
                    print(line)
                    exit(0)
                enumID = int(line[enumIDl + 13: enumIDr])
                valuel = line.find("value=\"")
                valuer = line.find("\"", valuel + 7)
                if valuel == -1 or valuer == -1:
                    continue
                value = int(line[valuel + 7: valuer])
                # CARDTYPE
                if enumID == 202:
                    CARDTYPE = value
            if "<zhCN>" in line:
                if enumID != 185 and enumID != 184:
                    continue
                index1 = line.find("<zhCN>")
                index2 = line.find("</zhCN>", index1 + 6)
                text = ""
                while (index2 == -1):
                    text += line[index1 + 6:-1]
                    line = f.readline()
                    fc.write(line)
                    index2 = line.find("</zhCN>")
                    index1 = -6
                text += line[index1 + 6: index2]
                if index1 == -1 or index2 == -1:
                    print("错误:" + line)
                    exit(0)
                if enumID == 185:
                    cardnamecn = html.unescape(text)

从指定目录转移有内容的Sim卡到指定位置(和1搭配使用)

import os
import shutil

def readFilename(file_dir):
    for root, dirs, files in os.walk(file_dir):
        return files, dirs, root


def findstring(pathfile):
    fp = open(pathfile, "r", encoding='UTF-8')  # 注意这里的打开文件编码方式
    strr = fp.read()
    # print strr.find("DoubleVec")
    if (strr.find("public") != -1):
        print('here?')
        return True
    return False


def startfind(files, dirs, root):
    for ii in files:
        # print(ii)
        # if ii.endswith('.lua'):
        try:
            if (findstring(root + "\\" + ii)):
                shutil.copy(root + "\\" + ii, "C:\\hs\\sim\\" + ii)
                print(ii)
        except Exception as err:
            print(err)
            continue

    for jj in dirs:
        fi, di, ro = readFilename(root + "\\" + jj)
        startfind(fi, di, ro)


if __name__ == '__main__':
    default_dir = u"H:\\Hearthbuddy\\Routines\\DefaultRoutine\\Silverfish\\cards"  # 设置默认打开目录
    file_path = default_dir  # th.expanduser(default_dir)))
    files, dirs, root = readFilename(file_path)
    startfind(files, dirs, root)

读取卡牌代码,输出策略中的卡牌特征函数

import base64
import win32clipboard as w
import win32con

def setText(aString):#写入剪切板
    w.OpenClipboard()
    w.EmptyClipboard()
    w.SetClipboardData(win32con.CF_UNICODETEXT, aString)
    w.CloseClipboard()

def read_varint(data):
    shift = 0
    result = 0
    while True:
        temp = data.pop(0)
        result |= (temp & 0x7f) << shift
        shift += 7
        if (temp & 0x80) == 0:
            break
    return  result

CardDB = {}
with open(r"H:\CardDefs.xml", 'r', encoding='utf-8') as f:
        while True:
            line = f.readline()
            if not line:
                break
            if r"</Entity>" in line:
                CardDB[numberID] = cardID
                cardID = ""
                numberID = 0

            if "<Entity CardID=\"" in line:
                index1 = line.find("<Entity CardID=\"")
                index2 = line.find("\"", index1 + 16)
                if index1 == -1 or index2 == -1:
                    print(line)
                    exit(0)
                cardID = line[index1 + 16: index2]
                index1 = line.find("ID=\"", index2)
                index2 = line.find("\"", index1 + 4)
                if index1 == -1 or index2 == -1:
                    print(line)
                    exit(0)
                numberID = int(line[index1 + 4: index2])

while True:
    try:
        code = input()
        codelist = list(base64.b64decode(code))
    except:
        continue
    if len(codelist) == 0:
        continue
    reserve = read_varint(codelist)
    if reserve != 0:
        continue
    version = read_varint(codelist)
    if version != 1:
        continue
    format = read_varint(codelist)
    heroes = []
    num_heroes = read_varint(codelist)
    for i in range(num_heroes):
        card_id = read_varint(codelist)
        heroes.append(CardDB[card_id])
    cards = []
    num_cards_x1 = read_varint(codelist)
    for i in range(num_cards_x1):
        card_id = read_varint(codelist)
        cards.append([CardDB[card_id], 1])

    num_cards_x2 = read_varint(codelist)
    for i in range(num_cards_x2):
        card_id = read_varint(codelist)
        cards.append([CardDB[card_id], 2])

    num_cards_xn = read_varint(codelist)
    for i in range(num_cards_xn):
        card_id = read_varint(codelist)
        count = read_varint(codelist)
        cards.append({CardDB[card_id], count})
    str = "public List<CardDB.cardIDEnum> ArchetypeCardSet = new List<CardDB.cardIDEnum>\n{\n"
    cardslen = len(cards)
    for i in range(cardslen):
        str += "\tCardDB.cardIDEnum." + cards[i][0]
        if i != cardslen:
            str += ",\n"
    str += "};"
    print(str)
    setText(str)

对于炉石兄弟策略编写的一些感悟 7月30日

1.关于惩罚
 我看到很多策略的编写者,包括我刚入门的时候,喜欢用惩罚来调整兄弟出牌,因为当时我认为兄弟的AI算法就是一个简单的BFS搜索 + 剪枝,只会考虑当前场面最优解。
 但现在发现其实兄弟并没有这么蠢,他是考虑了本回合 + 下一回合的操作(只要设置合理),这样前瞻的思路其实完全够用。所以我认为惩罚的作用应该是用于调整大于两回合,兄弟预料不到的前瞻操作。
比如:
1.休眠随从的一些特效。
2.一些卡牌为打出一些Combo而留牌。
3.为了赚取更大场面优势而留牌(比如图腾潮涌,有图腾时直接打出是对场面有益的,但是为了更好的场面所以要通过惩罚留牌)。

 那么惩罚该怎么写呢?下面是我的一些感想,希望能给大家提供一定的帮助
 之前看到弄的风风火火的破冰和火焰结界,但现在都没有看到一种合理的解决方法,这是为什么呢?
 我思考之后发现,很多人用惩罚去调整,反而效果适得其反。原因就是惩罚像是教小孩子的时候你对他施暴,而策略就是合理的教学,教给他怎么样才是对的。
 比如:火焰结界现在流行的版本添加的位置都是错误的,都是和猎人奥秘爆炸陷阱放在同一个地方,这个就属于模拟特效的问题,火焰结界是随从攻击后触发,而爆炸陷阱是攻击前触发,这两个放在一起,问题就出现了,兄弟认为用任意一个随从去攻击效果是一样的,反正都是无法造成伤害,而不是我们一般的思路,用攻击最高的随从去攻击。
 还有就是通过惩罚的一些弊端,比如你写惩罚,低血量的时候使用某个有治疗效果的卡牌,返回的是负数的惩罚值。其实返回负数的惩罚值就违背了惩罚的概念。当时这样写的时候只考虑到了低血量的时候使用可以快速的回血,符合我们平常打牌的思维,但是你没有想到的是,兄弟会想办法去构造这种低血量来为了得到这个负数的惩罚(比如火球术打脸),当然这种惩罚如果数额写小一点那还无所谓,因为会被策略中所调整,但是如果数额较大,比如30、50这种程度,那就非常不好了。
 所以我推荐惩罚值的编写最好是反常的思路,比如你正常思路中是低血量使用回血牌,而在惩罚值的编写中应当是高血量不使用回血牌,给一个正数的惩罚值,让兄弟知道这样是错误的,我认为这样才是正常的惩罚使用方法。
 当然这也还是有一些问题,我还是更加推荐使用策略中血量低于一定的血线则给出一定低的评分更好一些。

2.关于策略
这里的策略指的是behavior文件夹中的策略文件,这些策略大多用来给场面一个合理的评分。
那么有些兄弟就会说了,我的兄弟打出伏笔操作了,又不能通过惩罚来调整,那该怎么办呢?
我这里有两种方法:
1.检查SIM是否编写正确,是否按照真实情况去模拟了
2.调整策略中的对战场的评分,比如要加强留牌,那么就给手牌数量的权重调大,如果要加强场面,那就给单个随从的价值调大。

 确定SIM的正确性是非常重要的,包括早期兄弟官方写的卡牌也是错误频出,问题就在于没有测试,我认为写一张卡牌首先要保障其语法的正确性,再要保证其模拟的正确性,这才是一张合理正确的卡牌。而出牌速度的问题也是因为模拟错误所导致的,正是因为模拟错误从而导致兄弟认为场面和模拟的不一致,而重新计算。而这个场面模拟是很严格的,就连随从的位置顺序都很重要,比如亡语:召唤一个XXX随从,这样的卡牌的召唤位置一定要是m.zonepos - 1,才是正确的模拟,包括利用p.ownMinion.Count的都是不正确的,甚至有些随从战吼召唤是在随从的一左一右的,这种位置关系也一定要考虑。否则重新计算的时间是非常难熬的。
 但是有些随机性的问题是难以模拟的,比如抽一张牌,按一下图腾召唤技能,这样的操作下一步是完全未知的。这种牌是兄弟非常难把控的,原因就在于兄弟只能固定一种随机操作而围绕这种随机的操作的固定结果而计算出的定性结果,这样持续下去兄弟把这一回合的操作都计算完了,结果开始一操作就发现操作结果与模拟的不符合,然后又去重新计算。所以为什么兄弟不适合打弃牌术,我认为大概也是这个原因吧,这个问题来自于兄弟本身的算法架构,也很难修改。
 所以,盲目的追求操作速度其实意义没那么大,操作并没有那么长时间,真正长的时间应该是在大场面的计算上,而计算的问题正如我上面所说,应该要尽量写对结果可预料的卡牌编写正常的SIM,而对于具有随机性的卡牌也应该要考虑多种情况分类讨论。最简单的就是对斩杀的判定。

3.关于_settings.txt
 其中的maxwide我想说明一下,这个参数表明的是计算的深度,也就是数值越大,计算的越深入,我认为如果随机性强的卡组,比如弃牌术,应该把这个数字调低一些,因为基于错误的随机模拟上的大量计算是没有意义的。而如果是随机性弱的卡组,比如奥秘法等,这个数字可以适当的调高。这个原因呢,仔细品味我这篇文章,就自然知道了。

 如果有疑问的可以在评论区提出,欢迎大家指出我的错误,一起讨论学习!

Last Modified: March 27, 2021
Archives QR Code
QR Code for this page
Tipping QR Code
Leave a Comment

20 Comments
  1. AceYoji AceYoji

    前来学习前辈心得。

  2. xu xu

    怎么调试程序呀,test目录下的Aitest.cs怎么才能运行起来呢

    1. wjh wjh

      @xu您好,我有一段时间没有参与炉石兄弟的研究,有些不太记得了,不好意思。

  3. 李文浩 李文浩

    感谢博主,你的那篇兄弟目录分析对我入坑兄弟帮助很大@(真棒)

  4. Yt0110 Yt0110

    大佬666,新入坑兄弟的兄弟学习报道

  5. hello world hello world

    博主你好,按你最 Python 脚本 已经生成了 sim 和 CardDB_cardIDEnum.cs 文件
    CardDB_cardName.cs 相关脚本文件能分享下吗?

    1. wjh wjh

      @hello world缺了再手动添加

  6. road lamp road lamp

    读取卡牌代码,输出策略中的卡牌特征函数 这个弄完没反应啊

  7. Ground Ground

    博主你好,我按照你的博文添加休眠报错如下,请问是哪里的问题呢?
    silverfish_HB.cs(834,33) : error CS0029: 无法将类型“int”隐式转换为“bool”
    silverfish_HB.cs(835,25) : error CS0019: 运算符“>”无法应用于“bool”和“int”类型的操作数
    silverfish_HB.cs(843,45) : error CS0029: 无法将类型“int”隐式转换为“bool”
    m.dormant = entitiy.GetTag(GAME_TAG.DORMANT);//休眠 fix4
    if (m.dormant > 0)
    m.dormant = en.GetTag(GAME_TAG.SCORE_VALUE_1) - en.GetTag(GAME_TAG.SCORE_VALUE_2);

    Playfield.cs(4644,21) : error CS0019: 运算符“>”无法应用于“bool”和“int”类型的操作数
    Playfield.cs(4646,21) : error CS0023: 运算符“--”无法应用于“bool”类型的操作数
    Playfield.cs(4647,25) : error CS0019: 运算符“==”无法应用于“bool”和“int”类型的操作数
    if (m.dormant > 0 && ownturn == m.own)//苏醒 fix5
    m.dormant--;
    if (m.dormant == 0)

    1. wjh wjh

      @Grounddormant 的类型我定义为int类型,而不是bool

    2. Ground Ground

      @wjh这样啊,那请问这条定义应该怎么写啊,求指教

    3. Ground Ground

      @Ground我悟了,解决了,Minion.cs里dormant的类型为int。谢谢

  8. 无名 无名

    请问博主有xd新版本的下载链接吗?

    1. wjh wjh

      @无名贴吧里看看

    2. gg gg

      @wjh贴吧的推荐学习找不到之前的主页了,新版在哪下呀,还是现在弄淘宝店有偿分享了不

  9. 油泼面 油泼面

    已经改完了,没有红字,不过我没改调试,因为是新手。另外,谢谢大神。

    1. wjh wjh

      @油泼面加油!

  10. wjh wjh

    各位爆红字的可以把红字内容复制到记事本中
    搜索error cs,把搜索到的那一行贴上来,我来帮忙解决。
    发之前看一下其他人的红字,如果有人红字和你相同那就不要重复发了。

    成功搞定的人也可以评论一下让我看见你们哦!

    1. hunterhunter hunterhunter

      @wjh添加了 调试程序就爆红
      Compiler Error: h:\炉石兄弟汉化版2020.1.17 改 发现 祈求及其Sim\Routines\DefaultRoutine\Silverfish\ai\BoardTester.cs(1035,23) : error CS1061: 'HREngine.Bots.Hrtprozis' does not contain a definition for 'updateInvokedInfo' and no extension method 'updateInvokedInfo' accepting a first argument of type 'HREngine.Bots.Hrtprozis' could be found (are you missing a using directive or an assembly reference?)

    2. wjh wjh

      @hunterhunterBoardTester.cs 可以不用改,看你这个报错应该是Hrtprozis.cs没添加函数吧?