MENU

关于炉石兄弟的目录和文件介绍

March 13, 2020 • Read: 25882 • 炉石兄弟

更新日志

2020-08-01:修正一些描述错误,以及更新对于SIM中的弃牌函数理解的添加
2020-06-07,05-23:修正一些错误
2020-03-23:更新MiniSimulator.cs(ai目录)的内容
2020-03-21:修正并更新Minion.cs(ai目录)的内容,感谢魔王的骑士不正不歪 0_0!后天更新MiniSimulator.cs,这两天要考试正好休息一下。
2020-03-20:更新Minion.cs(ai目录)的内容,之后更新会更慢一些,先更新一些关于修复旧版兄弟的内容
2020-03-16:更新Hrtprozis.cs(ai目录)的内容
2020-03-15:更新CardDefs.xml(data目录)的内容
2020-03-14:更新_settings.txt、_combo.txt(behavior目录)、SIM事件和常用函数(card目录)和CardDB.cs、Handmanager.cs(ai目录)的内容
2020-03-13:更新Ai.cs(ai目录)和_mulligan.txt(behavior目录)的内容
2020-03-12:创建此页面,目的是为了引导大家对兄弟的代码有所理解。如果存在技术上的问题,请留言回复,也可以留言鼓励一下哦~

目录介绍

首先进入炉石兄弟的根目录,可以看到以下文件夹
以下文件值得我们关注,我会对其中代码一一说明。

  • Plugins(插件相关文件)
  • Routines (策略相关文件)

    • DefaultRoutine

      • Silverfish

        • ai

          • action.cs
          • ActionNormalizer.cs
          • Ai.cs
          • Behavior.cs
          • BehaviorControl.cs
          • BehaviorRush.cs
          • BehaviourMana.cs
          • BoardTester.cs
          • CardDB.cs
          • CardDB_cardIDEnum.cs
          • CardDB_cardName.cs
          • CardDB_getSimCard.cs
          • ComboBreaker.cs
          • Deckmanager.cs
          • EnemyTurnSimulator.cs
          • Handmanager.cs
          • Helpfunctions.cs
          • Hrtprozis.cs
          • Minion.cs
          • MiniSimulator.cs
          • MiniSimulatorNextTurn.cs
          • Movegenerator.cs
          • Mulligan.cs
          • PenalityManager.cs
          • PenTemplate.cs
          • Playfield.cs
          • Probabilitymaker.cs
          • Questmanager.cs
          • RulesEngine.cs
          • Settings.cs
          • SimTemplate.cs
          • TAGGS.cs
          • Weapon.cs
        • behavior

          • control

            • Behavior控场模式.cs
          • rush

            • Behavior怼脸模式.cs
        • cards
        • data

          • CardDefs.xml
        • penalties
        • UltimateLogs
        • DefaultRoutine.cs
        • DefaultRoutineSettings.cs
        • SettingsGui.xaml
        • silverfish_HB.cs
  • Hearthbuddy.exe (炉石兄弟主程序)


Action.cs(回合操作)

public enum actionEnum
{
    endturn = 0,
    playcard,
    attackWithHero,
    useHeroPower,
    attackWithMinion
}
public class Action
{
    public actionEnum actionType;
    public Handmanager.Handcard card;
    //public int cardEntitiy;
    public int place; //= target where card/minion is placed
    public Minion own;
    public Minion target;
    public int druidchoice; // 1 left card, 2 right card
    public int penalty;
    public int turn = -1;
    public int prevHpOwn = -1;
    public int prevHpTarget = -1;
}

这些是从文件中摘抄出来的一些重要内容,我分别来介绍。

首先是actionEnum枚举类型,在这个枚举类型中包括着不同的对战方式。

public enum actionEnum
{
    endturn = 0, // 结束此回合
    playcard, // 出一张牌,出的牌在card中
    attackWithHero, // 英雄进行攻击,target作为目标,own通常是我方英雄
    useHeroPower, 
    // 使用英雄技能,target作为目标
    // (英雄技能无攻击目标的英雄则为null,如Mage)
    attackWithMinion // 随从进行攻击,target作为目标,own是用于攻击的随从
}

接下来对每个每个成员进行介绍

public class Action
{
    actionEnum actionType; // 用于记录操作的类型。
    Handmanager.Handcard card; 
    // 记录出的牌,在attackWithHero和attackWithMinion操作中为null
    int place;
    // 用于记录位置,如actionType为playcard时就会有内容,
    // 比如出一个随从就会标记出放置的位置(最左边为0),
    // 如果是出一张法术牌的话,具体内容没有研究,应该是有赋值的

    Minion own;
    Minion target; 
    // 这两个成员,顾名思义一个是 我方随从(英雄),另一个是攻击目标。

    int druidchoice; 
    // 用于抉择牌选择
    // 如果是未修改过的兄弟,抉择牌的编号分别是(0:中间 1:左边 2:右边)
    int penalty; 
    // 惩罚值,对于这个操作给出多少的惩罚
    // 值越大越不推荐这样下,如果值为负数则是非常推荐。
    int turn = -1; 
    // 记录的应该是此回合中的第几步操作,初始值为-1 (不确定)
    int prevHpOwn = -1; // 不确定
    int prevHpTarget = -1; // 不确定
}

Ai.cs(AI调用和调试)

AI.cs中的内容比较复杂,涉及到相关的算法知识,如果只是萌新可以考虑跳过这一块。
Ai.cs中的一些成员属性就不在这里阐述了,之后在_setting.txt文件的设置中说明
直接来看Ai.cs中最核心的函数doallmoves

private void doallmoves(bool test, bool isLethalCheck)
{
    //set maxwide to the value for the first-turn-sim.
    foreach (EnemyTurnSimulator ets in enemyTurnSim)
    {
        ets.setMaxwide(true);
    }
    foreach (EnemyTurnSimulator ets in enemySecondTurnSim)
    {
        ets.setMaxwide(true);
    }

    //if (isLethalCheck) this.posmoves[0].enemySecretList.Clear();
    this.posmoves[0].isLethalCheck = isLethalCheck;
    this.mainTurnSimulator.doallmoves(this.posmoves[0]);

    bestplay = this.mainTurnSimulator.bestboard;
    float bestval = this.mainTurnSimulator.bestmoveValue;

    help.loggonoff(true);
    help.logg("-------------------------------------");
    if (bestplay.ruleWeight != 0) help.logg("ruleWeight " + bestplay.ruleWeight * -1);
    if (settings.printRules > 0)
    {
        String[] rulesStr = bestplay.rulesUsed.Split('@');
        foreach (string rs in rulesStr)
        {
            if (rs == "") continue;
            help.logg("rule: " + rs);
        }
    }
    help.logg("value of best board " + bestval);

    this.bestActions.Clear();
    this.bestmove = null;
    ActionNormalizer an = new ActionNormalizer();
    //an.checkLostActions(bestplay, isLethalCheck);
    if (settings.adjustActions > 0) an.adjustActions(bestplay, isLethalCheck);
    foreach (Action a in bestplay.playactions)
    {
        this.bestActions.Add(new Action(a));
        a.print();
    }

    if (this.bestActions.Count >= 1)
    {
        this.bestmove = this.bestActions[0];
        this.bestActions.RemoveAt(0);
    }
    this.bestmoveValue = bestval;

    if (bestmove != null && bestmove.actionType != actionEnum.endturn) // save the guessed move, so we doesnt need to recalc!
    {
        this.nextMoveGuess = new Playfield();

        this.nextMoveGuess.doAction(bestmove);
    }
    else
    {
        nextMoveGuess.mana = -100;
    }

    if (isLethalCheck)
    {
        this.lethalMissing = bestplay.enemyHero.armor + bestplay.enemyHero.Hp;//RR
        help.logg("missing dmg to lethal " + this.lethalMissing);
    }
}

接下来讲函数的每一步分解
首先看到传入函数的两个参数

private void doallmoves(bool test, bool isLethalCheck)

第一个参数test,显然是调试的时候为true
第二个参数isLethalCheck,这个参数是拿来干什么的呢?经过我对代码的跟踪,发现这个参数是拿来进行斩杀判定的,也就是当这里传入true之后,兄弟接下来的只会模拟一些有可能造成伤害的操作,来判定是否会斩杀,如果不能造成斩杀,则再次进行isLethalCheck = false的运算。而我们经常看到兄弟错斩的情况,就可以从这里入手来找解决的办法,具体解决办法在PenalityManager.cs处说明。

//set maxwide to the value for the first-turn-sim.
foreach (EnemyTurnSimulator ets in enemyTurnSim)
{
    ets.setMaxwide(true);
}
foreach (EnemyTurnSimulator ets in enemySecondTurnSim)
{
    ets.setMaxwide(true);
}

这一段代码有些人可能会看着有些懵,什么是Maxwide?什么是enemyTurnSim,什么是enemySecondTurnSim
我对这一代码的理解就是兄弟对接下来的状态搜索进行初始化操作。
而这里的Wide则就是指的是状态搜索中的宽度,还有另一个是Deep指的是状态搜索中的深度。

这里需要一些补充知识,如果对树的概念有所理解的可以跳过这部分内容。
搜索树
如图所示A节点 称作这棵树的根(root)
B C D E F G 节点 称作这棵树深度为1的层(Deep = 1),而这些节点的来源则是A节点,我们则称A节点为这些节点的父节点,而在搜索树中,这也意味着这些节点是由A节点的状态转移过来的。且在Deep = 1这一层的宽度为6(Wide = 6)

而在炉石兄弟中的root(A节点)则是不进行任何操作的时候,可以理解为是上个回合对方回合结束之后的场面。

在炉石兄弟中的Deep(深度)可以简单的理解为,第几个操作,比如一个回合我分别进行了3个操作。
1.用我的随从A攻击对面的英雄(E节点 Deep = 1)
2.使用英雄技能(J节点 Deep = 2)
3.结束回合(P节点 Deep = 3)

这三个操作就可以对应为上面的A -> E -> J -> P
这一条路,兄弟所做的就是对于每一步结束的时候计算评分(Behavior控场模式.cs中)谁更大一些,就选择哪一条路来做。

Wide则可以简单的理解为在某一深度的情况下,最多的操作方法。

现在终于可以说清楚了,什么是MaxWide了。
让我们来想象一个场景,我们手上有十张牌能出,场面上也有满随从,对面场面上也是满随从。
这个时候,我们操作有多少种可能性?emmm,我想一定是个非常大的数字。
所以这个时候的Wide就过于大了,而且对于每一个节点我们还要深入搜索下一层。这个增长是爆炸式的。
于是我们就需要MaxWide来限制兄弟的最大宽度来缩小搜索范围,来减少搜索时间,防止超时。
当然,在这个缩小搜索范围的过程中,漏掉了一些最优打法也是在所难免的。

所以,当你配置高的时候,调高MaxWide可以有效地提高AI智慧,当你配置低的时候,降低MaxWide可以有效地缩短搜索时间。

好像有点跑偏了,让我们回到代码。

this.posmoves[0].isLethalCheck = isLethalCheck;
this.mainTurnSimulator.doallmoves(this.posmoves[0]);

这两行就把isLathalCheck给到Playfield中,什么是Playfied?目前先简单的理解为场面的数据,之后会细讲。
然后再对mainTurnSimulator.doallmoves这个函数进行调用。
进入这个里面才是真正的搜索模拟运算,详见MiniSimulator.cs解析。

接下来的内容不是特别重要,以注释的形式讲解

bestplay = this.mainTurnSimulator.bestboard; // 上面函数计算之后的最优场面
float bestval = this.mainTurnSimulator.bestmoveValue; // 上面最优场面的评分

// 输出与rule相关信息
help.loggonoff(true);
help.logg("-------------------------------------");
if (bestplay.ruleWeight != 0) help.logg("ruleWeight " + bestplay.ruleWeight * -1); 
if (settings.printRules > 0)
{
    String[] rulesStr = bestplay.rulesUsed.Split('@');
    foreach (string rs in rulesStr)
    {
        if (rs == "") continue;
        help.logg("rule: " + rs);
    }
}
// 输出与rule相关信息结束
help.logg("value of best board " + bestval);// 输出最优场面评分

this.bestActions.Clear();// 清除上次回合计算的操作方法
this.bestmove = null;// 清除上次回合计算的操作方法

//接下来两行是对操作重排使得其先打出AOE伤害,在_setting.txt中设定,默认关闭,实际使用效果并不好
ActionNormalizer an = new ActionNormalizer();
if (settings.adjustActions > 0) an.adjustActions(bestplay, isLethalCheck);


foreach (Action a in bestplay.playactions)
{
    this.bestActions.Add(new Action(a));
    a.print();
}
//输出场面信息
if (this.bestActions.Count >= 1)
{
    this.bestmove = this.bestActions[0];
    this.bestActions.RemoveAt(0);
}
this.bestmoveValue = bestval;

//如果不是回合结束操作,则模拟一下下一个操作
if (bestmove != null && bestmove.actionType != actionEnum.endturn) // save the guessed move, so we doesnt need to recalc!
{
    this.nextMoveGuess = new Playfield();
    this.nextMoveGuess.doAction(bestmove);
}
else
{
    //如果是回合结束操作,下一个操作则不进行(不进行的方法是让法力值-100,使其无操作可做)
    nextMoveGuess.mana = -100;
}

//斩杀信息输出,输出斩杀所需的伤害
if (isLethalCheck)
{
    this.lethalMissing = bestplay.enemyHero.armor + bestplay.enemyHero.Hp;//RR
    help.logg("missing dmg to lethal " + this.lethalMissing);
}

这个文件中另一个重要的函数与调试AI息息相关。这里我直接在注释中写出我的一些理解

public List<double> autoTester(bool printstuff, string data = "", int mode = 0) //-mode: 0-all, 1-lethalcheck, 2-normal
{
    //游戏信息(场面)还原,
    List<double> retval = new List<double>();
    double calcTime = 0;
    help.logg("simulating board ");

    BoardTester bt = new BoardTester(data);
    if (!bt.datareaded) return retval;
    hp.printHero();
    hp.printOwnMinions();
    hp.printEnemyMinions();
    hm.printcards();
    
    
    //开始计算
    posmoves.Clear();
    Playfield pMain = new Playfield();
    pMain.print = printstuff;
    posmoves.Add(pMain);
    //输出场面信息
    foreach (Playfield p in this.posmoves)
    {
        p.printBoard();
    }
    help.logg("ownminionscount " + posmoves[0].ownMinions.Count);
    help.logg("owncardscount " + posmoves[0].owncards.Count);

    foreach (var item in this.posmoves[0].owncards)
    {
        help.logg("card " + item.card.name + " is playable :" + item.canplayCard(posmoves[0], true) + " cost/mana: " + item.manacost + "/" + posmoves[0].mana);
    }
    help.logg("ability " + posmoves[0].ownHeroAblility.card.name + " is playable :" + posmoves[0].ownHeroAblility.card.canplayCard(posmoves[0], 2, true) + " cost/mana: " + posmoves[0].ownHeroAblility.card.getManaCost(posmoves[0], 2) + "/" + posmoves[0].mana);
    //输出场面信息结束
    DateTime strt = DateTime.Now;
    // 斩杀判定
    if (mode == 0 || mode == 1)
    {
        // 可以看到这个函数正是我们上面所讲的,这里第二个参数传入了true
        // 则说明这里这个分支则是兄弟进行扎煞判定的地方
        doallmoves(false, true);
        calcTime = (DateTime.Now - strt).TotalSeconds;
        help.logg("calculated " + calcTime);
        retval.Add(calcTime);
    }
    // 这里要补充一下,如果兄弟判定对面血量为负数(已斩杀),则会将场面评分提升到10000以上。
    // 而这里的bestmoveValue > 5000,则说明场面已经大概可以斩杀了。
    // 但是为什么是五千不是一万?
    // 这个问题的原因在于兄弟是会对当前回合和我方下一回合进行模拟的,而默认的权重分配是0.5
    // 如果这个回合斩杀了,兄弟可能就不会对下个回合进行模拟,所以如果这个回合评分为10000 * 0.5 + 0 * 0.5 = 5000
    // 以上只是个人猜测,有少量的代码可证明,知道原因的可以在下面评论哦
    if (Settings.Instance.berserkIfCanFinishNextTour > 0 && bestmoveValue > 5000)
    {

    }
    else if (bestmoveValue < 10000)
    {
        // 到了这里,就是正常的模拟了,会正常模拟所有可能的操作方法。
        // 如果是调试,应该进入这里的**doallmoves(false, false);**函数进行调试。
        // normal
        if (mode == 0 || mode == 2)
        {
            posmoves.Clear();
            pMain = new Playfield();
            pMain.print = printstuff;
            posmoves.Add(pMain);
            strt = DateTime.Now;
            doallmoves(false, false);
            calcTime = (DateTime.Now - strt).TotalSeconds;
            help.logg("calculated " + calcTime);
            retval.Add(calcTime);
        }
    }
    
    if (printstuff)
    {
        this.mainTurnSimulator.printPosmoves();
        simmulateWholeTurn();
        help.logg("calculated " + calcTime);
    }

    return retval;
}

这个文件的内容终于写完了,比我想象中的要快一些。
对于搜索树的介绍是我一时兴起,写完之后才发现这个AI.cs文件并不是AI计算的主要文件,那就充当MiniSimulator.cs(AI功能)的铺垫吧。

CardDB.cs(卡牌数据)

public enum cardtype
{
    NONE, //未知
    MOB = 4, //随从
    SPELL = 5, //法术
    WEAPON = 7, //武器
    HEROPWR = 10, //英雄技能
    ENCHANTMENT = 6, //增幅(例如:变形术,救赎,力量的代价,自然之力的附加效果)
    HERO = 3, //英雄卡
}

摘入几个比较重要ErrorType

public enum ErrorType2
{
    REQ_MINION_TARGET = 1, 随从目标
    REQ_FRIENDLY_TARGET = 2, 友方目标
    REQ_ENEMY_TARGET = 3, 敌方目标
    REQ_DAMAGED_TARGET = 4, 损伤目标
    REQ_MAX_SECRETS = 5, 最大奥秘
    REQ_FROZEN_TARGET = 6, 冻结目标
    REQ_CHARGE_TARGET = 7, 冲锋目标
    REQ_TARGET_MAX_ATTACK = 8, 最大攻击力目标 有param=
    REQ_NONSELF_TARGET = 9, 非自己目标
    REQ_TARGET_WITH_RACE = 10, 种族目标 有param=
    REQ_TARGET_TO_PLAY = 11, 需要目标
    REQ_NUM_MINION_SLOTS = 12, 随从数目插槽 有param=
    REQ_WEAPON_EQUIPPED = 13, 武器装备,需要武器
    REQ_HERO_TARGET = 17, 英雄目标
    REQ_TARGET_IF_AVAILABLE = 22, 有目标如果用(抉择星辰降落,巫医)
    REQ_MINIMUM_ENEMY_MINIONS = 23, 最少的敌方随从 有param=
    REQ_TARGET_FOR_COMBO = 24, 连击有目标
    REQ_HERO_OR_MINION_TARGET = 33, 英雄或随从目标
    REQ_TARGET_MIN_ATTACK = 41, 最小攻击力,有param=
    REQ_MINIMUM_TOTAL_MINIONS = 45, 需要最少随从数目,有param=
    REQ_MUST_TARGET_TAUNTER = 46, 目标必须是嘲讽
    REQ_UNDAMAGED_TARGET = 47, 目标必须是未受伤的
    REQ_TARGET_IF_AVAILABLE_AND_DRAGON_IN_HAND = 51, 有龙牌在手才有目标
    REQ_LEGENDARY_TARGET = 52, 传说目标
    REQ_FRIENDLY_MINION_DIED_THIS_TURN = 53,需要一个死亡的友方随从在当前回合死亡
    REQ_FRIENDLY_MINION_DIED_THIS_GAME = 54, 需要一个死亡的友方随从
}

摘入几个比较重要的Card成员

public class Card
{
    public cardName name = cardName.unknown;//名称
    public int race = 0;//种族
    //14鱼人 15恶魔 17机械 18元素 20野兽 21图腾 23海盗 24龙
    public int rarity = 0;//稀有度
    //1普通白 2基础无 3稀有蓝 4史诗紫 5传说橙
    public int cost = 0;//费用
    public int Class = 0;//职业
    //2德鲁伊 3猎人 4法师 5圣骑士 6牧师 7潜行者 8萨满 9术士 10战士 11梦境牌 12中立
    public cardtype type = CardDB.cardtype.NONE;//类别
    public int Attack = 0; //攻击力
    public int Health = 0; //血量
    public int Durability = 0; //耐久值
    public bool tank = false; //嘲讽
    public bool Silence = false; //沉默
    public bool choice = false; //抉择
    public bool windfury = false; //风怒
    public bool poisonous = false; //剧毒
    public bool lifesteal = false; //吸血
    public bool reborn = false; //复生
    public bool deathrattle = false; //亡语
    public bool battlecry = false; //战吼
    public bool discover = false; //发现
    public bool oneTurnEffect = false;
    public bool Enrage = false; //愤怒
    public bool Aura = false; //光环
    public bool Combo = false; //连击
    public int overload = 0; //超载
    public bool untouchable = false; //不可被攻击
    public bool Stealth = false; //潜行
    public bool Freeze = false; //冰冻
    public bool Shield = false; //圣盾
    public bool Charge = false; //突袭
    public bool Modular = false; //磁力
    public bool Rush = false; //冲锋
    public bool Secret = false; //奥秘
    public bool Quest = false; //任务
    public bool Morph = false; //变形
    public bool Spellpower = false; //法术伤害
    public bool Inspire = false; //激励
}

添加新卡机制、修复卡牌特效修改点

修改手牌费用识别,比如海巨人

public int getManaCost(Playfield p, int currentcost)
public int calculateManaCost(Playfield p)

修改攻击目标,比如矮人神射手

public List<Minion> getTargetsForCard(Playfield p, bool isLethalCheck, bool own)
public List<Minion> getTargetsForHeroPower(Playfield p, bool own)

修正bug

此处内容需要评论回复后方可阅读

Handmanager.cs(手牌管理)

public class Handcard
{
    public int position = 0; //手牌的位置
    public int entity = -1; //炉石传说内部entity编号
    public int manacost = 1000; //花费费用,但获取卡牌费用要用getManaCost(Playfield p)函数
    public int addattack = 0; //增加的攻击力,如风驰电掣的SIM中就使其 +1
    public int addHp = 0; //增加的血量
    public CardDB.Card card; //卡牌,指向CardDB.cs
    public Minion target; //目标
    public int elemPoweredUp = 0; //上回合是否使用元素牌
    public int extraParam2 = 0; //扩展参数2,可以用来记录一些此卡需要的特殊数据
    public bool extraParam3 = false; //扩展参数3
    //读取卡牌法力值
    public int getManaCost(Playfield p)
    {
        return this.card.getManaCost(p, this.manacost);
    }
    //判定卡牌是否能够使用
    public bool canplayCard(Playfield p, bool own)
    {
        return this.card.canplayCard(p, this.manacost, own);
    }
}

Handmanager.cs内容不多,所以介绍很简短。

Hrtprozis.cs(对局信息)

Hrtprozis是兄弟中非常重要的一个类,记录着从兄弟内部数据中获取到的各种信息

public class Hrtprozis
{
    public int pId = 0; //唯一id
    public int attackFaceHp = 15; //打脸血量
    public int ownHeroFatigue = 0; //我方疲劳
    public int ownDeckSize = 30; //我方牌库数量
    public int enemyDeckSize = 30; //敌方牌库数量
    public int enemyHeroFatigue = 0; //敌方疲劳 
    public int gTurn = 0; //第几回合
    public int gTurnStep = 0; //第几个操作

    public int ownHeroEntity = -1; //我方英雄Entity
    public int enemyHeroEntitiy = -1; //敌方英雄Entity
    public DateTime roundstart = DateTime.Now; //回合开始时间
    public int currentMana = 0; //当前法力值

    public int heroHp = 30, enemyHp = 30; //我方英雄血量,敌方英雄血量
    public int heroAtk = 0, enemyAtk = 0; //我方英雄攻击力,敌方英雄攻击力
    public int heroDefence = 0, enemyDefence = 0; //我方英雄护甲,敌方英雄护甲
    public bool ownheroisread = false; //我方英雄是否可以攻击
    public int ownHeroNumAttacksThisTurn = 0; //我方英雄此回合攻击了几次
    public bool ownHeroWindfury = false; //我方英雄是否风怒
    public bool herofrozen = false; //我方英雄是否冻结
    public bool enemyfrozen = false; //敌方英雄是否冻结

    public List<CardDB.cardIDEnum> ownSecretList = new List<CardDB.cardIDEnum>(); //我方奥秘列表
    public int enemySecretCount = 0; //敌方奥秘数量
    public Dictionary<int, CardDB.cardIDEnum> DiscoverCards = new Dictionary<int, CardDB.cardIDEnum>(); //发现的卡牌
    public Dictionary<CardDB.cardIDEnum, int> turnDeck = new Dictionary<CardDB.cardIDEnum, int>(); //牌库的卡牌
    private Dictionary<int, CardDB.cardIDEnum> deckCardForCost = new Dictionary<int, CardDB.cardIDEnum>(); //指定费用的卡牌
    public bool noDuplicates = false; //牌库无重复(宇宙)

    private int numTauntCards = -1; //牌库有几张嘲讽卡
    private int numDivineShieldCards = -1; //牌库有几张圣盾卡
    private int numLifestealCards = -1;  //牌库有几张吸血卡
    private int numWindfuryCards = -1; //牌库有几张风怒卡

    public bool setGameRule = false; //设置Rule

    public HeroEnum heroname = HeroEnum.None, enemyHeroname = HeroEnum.None; //我方英雄职业,敌方英雄职业
    public string heronameingame = "", enemyHeronameingame = ""; //我方英雄名称,敌方英雄名称
    public TAG_CLASS ownHeroStartClass = TAG_CLASS.INVALID; //我方英雄职业
    public TAG_CLASS enemyHeroStartClass = TAG_CLASS.INVALID; //敌方英雄职业
    public CardDB.Card heroAbility; //我方英雄技能
    public bool ownAbilityisReady = false; //我方英雄技能是否准备完成
    public int ownHeroPowerCost = 2; //我方英雄技能费用
    public CardDB.Card enemyAbility; //敌方英雄技能
    public int enemyHeroPowerCost = 2; //敌方英雄技能费用
    public int numOptionsPlayedThisTurn = 0; //此回合中的操作数
    public int numMinionsPlayedThisTurn = 0; //此回合中的随从数
    public CardDB.cardIDEnum OwnLastDiedMinion = CardDB.cardIDEnum.None; //我方最后死亡的随从

    public int cardsPlayedThisTurn = 0; //此回合中的出牌数,可以用于判定连击
    public int ueberladung = 0; //过载水晶
    public int lockedMana = 0; //锁定水晶
    public int ownMaxMana = 0; //我方最大法力值
    public int enemyMaxMana = 0; //敌方最大法力值
    
    public Minion ownHero = new Minion(); //我方英雄
    public Minion enemyHero = new Minion(); //敌方英雄
    public Weapon ownWeapon = new Weapon(); //我方武器
    public Weapon enemyWeapon = new Weapon(); //敌方武器
    public List<Minion> ownMinions = new List<Minion>(); //我方随从
    public List<Minion> enemyMinions = new List<Minion>(); //敌方随从
    public Dictionary<int, IDEnumOwner> LurkersDB = new Dictionary<int, IDEnumOwner>(); //潜行随从

    public int anzOgOwnCThunHpBonus = 0; //克苏恩血量
    public int anzOgOwnCThunAngrBonus = 0; //克苏恩攻击力
    public int anzOgOwnCThunTaunt = 0; //克苏恩嘲讽
    public int anzOwnJadeGolem = 0; //我方魔像计数器
    public int anzEnemyJadeGolem = 0; //敌方魔像计数器
    public int OwnInvoke = 0; //我方祈求数
    public int EnemyInvoke = 0; //敌方祈求数
    public int ownCrystalCore = 0; //魔王的骑士:水晶核心,任务贼的任务奖励,在本局对战的剩余时间内,你的所有随从变为 4/4。
    public bool ownMinionsInDeckCost0 = false; //我方牌库中是否有0费随从
    public int anzOwnElementalsThisTurn = 0; //在此回合中使用元素牌
    public int anzOwnElementalsLastTurn = 0; //上一回合使用元素牌
    public int ownElementalsHaveLifesteal = 0; //我方元素牌具有吸血
    private int ownPlayerController = 0; //我方玩家控制?
    
    public PenalityManager penman; //惩罚管理
    public Settings settings; //设置管理
    Helpfunctions help; //调试输出
    CardDB cdb; //卡牌数据
}

接下来是介绍这个类中一些中能用到的函数

//英雄ID转到名称
public string heroIDtoName(string s)

//英雄名称转到枚举,留牌文件就是用这个函数进行转换的,由此可见圣骑士的名称用pala和paladin是一样的
public HeroEnum heroNametoEnum(string s)
{
    switch (s)
    {
        case "all": return HeroEnum.all;
        case "druid": return HeroEnum.druid;
        case "hunter": return HeroEnum.hunter;
        case "mage": return HeroEnum.mage;
        case "pala": return HeroEnum.pala;
        case "paladin": return HeroEnum.pala;
        case "priest": return HeroEnum.priest;
        case "shaman": return HeroEnum.shaman;
        case "thief": return HeroEnum.thief;
        case "rogue": return HeroEnum.thief;
        case "maievshadowsong": return HeroEnum.thief;
        case "warlock": return HeroEnum.warlock;
        case "warrior": return HeroEnum.warrior;
        case "lordjaraxxus": return HeroEnum.lordjaraxxus;
        case "ragnarosthefirelord": return HeroEnum.ragnarosthefirelord;
        default: return HeroEnum.None;
    }
}

//英雄枚举转到种类
public TAG_CLASS heroEnumtoTagClass(HeroEnum he)

//英雄种类转到枚举
public HeroEnum heroTAG_CLASSstringToEnum(string s)

//读取牌库中X费的卡牌
public CardDB.cardIDEnum getDeckCardsForCost(int cost)

//读取牌库中指定特性的卡牌(GAME_TAGs.TAUNT,GAME_TAGs.DIVINE_SHIELD,GAME_TAGs.LIFESTEAL,GAME_TAGs.WINDFURY)
public int numDeckCardsByTag(GAME_TAGs tag)

//输出当前场面上的信息到日志
public void printHero()

//输出我方随从信息到日志
public void printOwnMinions()

//输出敌方随从信息到日志
public void printEnemyMinions()

//输出我方牌库信息(兄弟的牌库读取时好时坏)
public void printOwnDeck()

Minion.cs(随从信息)

public class Minion
{
    public int anzGotDmg = 0; //受到伤害次数
    public int GotDmgValue = 0; //受到伤害总和
    public int anzGotHealed = 0; //受到治疗次数
    public int GotHealedValue = 0; //受到治疗总和
    public bool gotInspire = false; //得到激励
    public bool isHero = false; //是否为英雄
    public bool own; //是否为己方
    public int pID = 0; //PID
    public CardDB.cardName name = CardDB.cardName.unknown; //随从名称
    public TAG_CLASS cardClass = TAG_CLASS.INVALID; //随从归属职业
    public int synergy = 0; //职业契合度,即各种族(机械鱼人恶魔野兽)与职业相关性
    public Handmanager.Handcard handcard; //手牌信息,如果Minion中储存的内容不够的话,可以调用这个获取更多的信息,例如 m.handcard.card
    public int entitiyID = -1; //实例ID
    public int zonepos = 0; //随从放置位置
    public CardDB.Card deathrattle2; //亡语2号,比如其他随从的亡语技能转移
    public bool playedThisTurn = false; //在这回合使用
    public bool playedPrevTurn = false; //在上回合使用
    public int numAttacksThisTurn = 0; //这回合攻击了几次
    public bool immuneWhileAttacking = false; //攻击时免疫
    public bool allreadyAttacked = false; //已经攻击过
    public bool shadowmadnessed = false; //暗影狂乱,直到回合结束,获得一个攻击力小于或等于3的敌方随从的控制权
    public bool destroyOnOwnTurnStart = false; //我方回合开始被消灭
    public bool destroyOnEnemyTurnStart = false; //敌方回合开始被消灭
    public bool destroyOnOwnTurnEnd = false; //我方回合结束被消灭
    public bool destroyOnEnemyTurnEnd = false; //敌方回合结束被消灭
    public bool changeOwnerOnTurnStart = false; //回合开始时变更所有权,如暗影狂乱结束后的归还
    public bool conceal = false; //隐藏(直到你的下个回合,使所有友方随从获得潜行)
    public int ancestralspirit = 0; //先祖之魂,使一个随从获得“亡语:再次召唤该随从。
    public int desperatestand = 0; //殊死一搏,使一个随从获得“亡语:回到战场,并具有1点生命值。”
    public int souloftheforest = 0; //丛林之魂,使你的所有随从获得“亡语:召唤一个2/2的树人”。
    public int stegodon = 0; //剑龙
    public int livingspores = 0; //活性孢子 亡语:召唤两个1/1的植物。
    public int explorershat = 0; //探险帽 使一个随从获得 + 1/+1,亡语:将一个探险帽置入你的手牌。
    public int returnToHand = 0; //回到手牌
    public int infest = 0; //寄生感染 使你的所有随从获得 “亡语:随机将一张野兽牌置入你的手牌”。

    public int ownBlessingOfWisdom = 0; //我方智慧祝福
    public int enemyBlessingOfWisdom = 0; //敌方智慧祝福
    public int ownPowerWordGlory = 0; //我方真言术:耀
    public int enemyPowerWordGlory = 0; //敌方真言术:耀
    public int spellpower = 0; //法术强度

    public bool cantBeTargetedBySpellsOrHeroPowers = false; //无法成为法术或英雄技能的目标
    public bool cantAttackHeroes = false; //无法攻击英雄
    public bool cantAttack = false; //无法攻击

    public int Hp = 0; //当前血量
    public int maxHp = 0; //最大血量
    public int armor = 0; //护甲值(英雄)

    public int Angr = 0; //攻击力
    public int AdjacentAngr = 0; //相邻buff攻击力加成
    public int tempAttack = 0; //一回合攻击力加成
    public int justBuffed = 0; //?

    public bool Ready = false; //攻击准备就绪

    public bool taunt = false; //嘲讽
    public bool wounded = false; //受伤

    public bool divineshild = false; //圣盾
    public bool windfury = false; //风怒
    public bool frozen = false; //冻结
    public bool stealth = false; //隐身
    public bool immune = false; //免疫
    public bool untouchable = false; //无法被攻击
    public bool exhausted = false; //无法攻击?
    public bool lifesteal = false; //吸血
    public bool modular = false; //磁力
    public int charge = 0; //冲锋
    public int rush = 0; //突袭
    public int hChoice = 0; //?
    public bool poisonous = false; //剧毒
    public bool cantLowerHPbelowONE = false; //血量无法低于1

    public bool silenced = false; //沉默
    public bool playedFromHand = false; //从手牌中打出
    public bool extraParam = false; //扩展参数1
    public int extraParam2 = 0; //扩展参数2
}

下面还有几个常用的函数

//受到伤害
public void getDamageOrHeal(int dmg, Playfield p, bool isMinionAttack, bool dontCalcLostDmg)
//随从死亡
public void minionDied(Playfield p)
//更新状态
public void updateReadyness()
//被沉默
public void becomeSilence(Playfield p)
//攻击之后的状态
public Minion GetTargetForMinionWithSurvival(Playfield p, bool own)
//特殊随从的特效添加位置
public void loadEnchantments(List<miniEnch> enchants, int ownPlayerControler)

MiniSimulator.cs(AI功能)

本篇文章请结合上面的Ai.cs一起学习
首先来介绍非常重要且核心的函数doallmoves,为了方便学习代码,我将一些不必要的代码做了省略。
建议打开vs进行单步跟踪着看

//val是value的简称
public float doallmoves(Playfield playf)
{
    print = playf.print; //确定是否输出场面,记录在print中
    this.isLethalCheck = playf.isLethalCheck; //是否为伤害检查
    enoughCalculations = false; //计算是否足够,深度大于最大深度,计算场面数大于最大宽度时为true
    botBase = Ai.Instance.botBase; //策略文件类
    this.addToPosmoves(playf); //将当前场面加入到状态队列
    bool havedonesomething = true; //是否无步骤可出,例如:法力值不够出任何牌,随从全部已经攻击
    List<Playfield> temp = new List<Playfield>(); //这一回合的状态队列
    int deep = 0; //深度
    bestoldval = -20000000; //最小的val,用于标记是否已经计算过场面val
    while (havedonesomething)
    { 
        temp.AddRange(this.posmoves);
        havedonesomething = false;
        //startEnemyTurnSimThread 函数非常重要,AI的核心内容,看下面的介绍
        if (print) startEnemyTurnSimThread(temp, 0, temp.Count);
        else
        {
            //似乎是启用多线程的形式计算
            Parallel.ForEach(Partitioner.Create(0, temp.Count),
                 range =>
                 {
                     startEnemyTurnSimThread(temp, range.Item1, range.Item2);
                 });
        }

        foreach (Playfield p in temp)
        {
            if (this.totalboards > 0) this.calculated += p.nextPlayfields.Count; //计算总的场面数,用于剪枝
            if (this.calculated <= this.totalboards) //如果宽度小于设定值则继续计算
            {
                this.posmoves.AddRange(p.nextPlayfields); //将操作后的PlayField进入队列,方便下一次运算
            }

            float pVal = botBase.getPlayfieldValue(p);
            //计算场面的val,如果是调试的话跟可以进去看看,不在调试中跟进去的是模版文件
            //里面开头的第一句话便是  if (p.value >= -2000000) return p.value;
            //和上面的bestoldval = -20000000;对应来看就能理解
            //意思就是防止多次运算,这个场面的val如果大于-2000000(已经计算过val)则直接返回
            
            if (pVal > bestoldval)
            {
                //如果新计算的val大于以前记录的最大val,则更新最大的val,并且记录场面信息
                bestoldval = pVal;
                bestold = p;
                bestoldDuplicates.Clear();
            }
            else if (pVal == bestoldval) bestoldDuplicates.Add(p); //如果val相等则加入bestoldDuplicates,方便之后调用
        }

        if (isLethalCheck && bestoldval >= 10000) this.posmoves.Clear(); 
        //如果bestval大于等于10000则意味着兄弟计算出当前场面可以斩杀
        //对应的策略中的代码是 if (p.enemyHero.Hp <= 0) retval = 10000;

        if (this.posmoves.Count > 0) havedonesomething = true; //如果还有其他状态可以模拟,则继续运算
    
        cuttingposibilities(isLethalCheck); //去除重复的PlayField,下面会讲解
        deep++;
        //下面两行就是判断计算的数量有没有大于设定的值,如果大于则不进行运算了
        if (this.calculated > this.totalboards) enoughCalculations = true;
        if (deep >= this.maxdeep) enoughCalculations = true;
    }
    
    if (this.dirtyTwoTurnSim > 0 && !twoturnfields.Contains(bestold)) twoturnfields.Add(bestold);
    if (!isLethalCheck && bestoldval < 10000) doDirtyTwoTurnsim();

    if (posmoves.Count >= 1)
    {
        //把所有的场面根据val进行排序,将高分的排在前面
        posmoves.Sort((a, b) => botBase.getPlayfieldValue(b).CompareTo(botBase.getPlayfieldValue(a)));
        Playfield bestplay = posmoves[0];
        float bestval = botBase.getPlayfieldValue(bestplay);
        int pcount = posmoves.Count;
        for (int i = 1; i < pcount; i++)
        {
            float val = botBase.getPlayfieldValue(posmoves[i]);
            if (bestval > val) break;
            if (posmoves[i].cardsPlayedThisTurn > bestplay.cardsPlayedThisTurn) continue; 
            else if (posmoves[i].cardsPlayedThisTurn == bestplay.cardsPlayedThisTurn)
            {
                if (bestplay.optionsPlayedThisTurn > posmoves[i].optionsPlayedThisTurn) continue; 
                else if (bestplay.optionsPlayedThisTurn == posmoves[i].optionsPlayedThisTurn && bestplay.enemyHero.Hp <= posmoves[i].enemyHero.Hp) continue;
            }
            bestplay = posmoves[i];
            bestval = val;
        }
        this.bestmove = bestplay.getNextAction();
        this.bestmoveValue = bestval;
        this.bestboard = new Playfield(bestplay);
        this.bestboard.guessingHeroHP = bestplay.guessingHeroHP;
        this.bestboard.value = bestplay.value;
        this.bestboard.hashcode = bestplay.hashcode;
        bestoldDuplicates.Clear();
        //正常退出从这里返回
        return bestval;
    }
    this.bestmove = null;
    this.bestmoveValue = -100000;
    this.bestboard = playf;
    //这里是异常退出返回的值
    return -10000;
}
//对于这个函数名称我不是特别的理解,似乎应该是ownTurnSimThread
private void startEnemyTurnSimThread(List<Playfield> source, int startIndex, int endIndex)
{
    int berserk = Settings.Instance.berserkIfCanFinishNextTour;
    int printRules = Settings.Instance.printRules;
    for (int i = startIndex; i < endIndex; i++)
    {
        Playfield p = source[i];
        if (p.complete || p.ownHero.Hp <= 0) { }
        else if (!enoughCalculations)
        {
            //从这里进入调用getMoveList函数,这个函数返回的是兄弟下一步所有可以做的操作
            //如果调试到这里,输出actions,发现没有你想要的Action就可以跟进去看看
            //一般来说,法力值不够出的牌会直接省略,如果正常操作能够出的牌没出,
            //则说明,惩罚值大于500,操作直接被省略,这是我调试的经验
            //建议直接跟进去看寻找原因,可以加深理解
            List<Action> actions = movegen.getMoveList(p, usePenalityManager, useCutingTargets, true);

            if (printRules > 0) p.endTurnState = new Playfield(p);
            //读取到actions后接下来对每个步骤进行模拟
            //从而得到操作之后的场面并且计算val值
            foreach (Action a in actions)
            {
                Playfield pf = new Playfield(p);
                pf.doAction(a);
                pf.evaluatePenality += - pf.ruleWeight + RulesEngine.Instance.getRuleWeight(pf);
                if (pf.ownHero.Hp > 0 && pf.evaluatePenality < 500) p.nextPlayfields.Add(pf);
            }
        }

        if (this.isLethalCheck)
        {
            //可以斩杀
            if (berserk > 0)
            {
                //结束回合,接下来模拟敌方下一回合的操作
                p.endTurn();
                if (p.enemyHero.Hp > 0)
                {
                    bool needETS = true;
                    //如果对面没有嘲讽且我方随从全部可以攻击则进行不模拟
                    if (p.anzEnemyTaunt < 1) foreach (Minion m in p.ownMinions) { if (m.Ready) { needETS = false; break; } }
                    else
                    {
                        if (p.anzOwnTaunt < 1) foreach (Minion m in p.ownMinions) { if (m.Ready) { needETS = false; break; } }
                    }
                    //从这里进入模拟敌方下一回合的操作
                    if (needETS) Ai.Instance.enemyTurnSim[threadnumber].simulateEnemysTurn(p, this.simulateSecondTurn, playaround, false, playaroundprob, playaroundprob2);
                }
            }

            p.complete = true;

        }
        else
        {
            p.endTurn();
            //这里和上面的区别就在于不进行判定是否需要模拟敌方的下个回合和一些判定
            if (p.enemyHero.Hp > 0)
            {
                Ai.Instance.enemyTurnSim[threadnumber].simulateEnemysTurn(p, this.simulateSecondTurn, playaround, false, playaroundprob, playaroundprob2);
                //如果val <= -10000 再进入判断
                if (p.value <= -10000)
                {
                    bool secondChance = false;
                    foreach (Action a in p.playactions)
                    {
                        if (a.actionType == actionEnum.playcard)
                        {
                            //这里判定一下是否出了战吼相关的牌,如果有的话 val += 1500
                            if (pen.cardDrawBattleCryDatabase.ContainsKey(a.card.card.name)) secondChance = true;
                        }
                    }
                    if (secondChance) p.value += 1500;
                }
            }
            p.complete = true;
        }
        //计算一下p的val值
        botBase.getPlayfieldValue(p);
    }
}

最后给出自己的一些调试经验

如果能够斩杀却没有斩杀
1.考虑对面有没有奥秘,是否打开了防奥秘,如果打开了防奥秘,则因为冰箱的缘故不会打脸破除奥秘
2.如果对面没有奥秘,考虑在PenalityManager.cs中的几个Database中加入对应的数据

更好的下法没有用,直接跳过了回合
1.当场面过差时,兄弟会选择直接跳过回合,反正必输了。
2.场面过大(很多随从,很多手牌)的时候,兄弟计算很久,最后可能会因为上面所说的计算数量大于MaxWide和MaxDeep而结束了运算,可以考虑在_setting.txt(behavior/_setting.txt中有介绍)中把相应的数据调高。
3.跟进List<Action> actions = movegen.getMoveList(p, usePenalityManager, useCutingTargets, true);进行调试

各种打自己人,给对面加增益buff
思考一个问题,为什么兄弟会这样做?是因为计算出的评分比原来的要高,比如我设置每个奥秘加十的评分,兄弟就会用一些技能来打疯狂的科学家来创造奥秘。那为什么会这样做呢?无非应该是编写兄弟策略时对惩罚使用不当,希望大家可以通过调试来解决问题,并且分享出来。

PenalityManager.cs(惩罚管理)

待更新

Playfield.cs(场面信息)

待更新

Questmanager.cs(任务管理)

兄弟目前的任务管理不支持支线任务,在我更新之后我会教大家怎么修改兄弟来支持支线任务

_mulligan.txt(留牌配置)

此内容是对贴吧用户:wayne0036所发的帖子进行的整理。

_mulligan.txt是放在策略目录下的文件,如果文件夹下不存在此文件则需要自己创建一个。
如文件位置:Routines\DefaultRoutine\Silverfish\behavior\策略名称\_mulligan.txt

文件格式:

卡牌ID;己方职业;对方职业;留牌策略;/条件

卡牌ID:

卡牌ID含义
GAME_005幸运币
CFM_066暗金教侍从
ULD_239火焰结界

以上只是随便找了几个举例,关于其他的卡牌信息可以在FBIGAME炉石数据库查询。

职业信息:

职业简称含义
druid德鲁伊
hunter猎人
mage法师
pala圣骑士
priest牧师
thief盗贼
shaman萨满
warlock术士
warrior战士
demonhunter恶魔猎手
None任意职业

留牌策略:

留牌策略含义
Hold:1留一张
Hold:2留两张
Discard:2一张不留

条件:
指的是当手牌上还有XXX(填写ID)卡时进行留牌(GAME_005是游戏中的幸运币,后接这个条件就可以判断是否是后手了)。

注意:
留牌文件必须是使用UTF-8编码,否则会读取错误,如果读取错误了可以考虑看一下编码
费用小于三的会自动留牌,费用大于三的会自动弃掉。
也就是说,费用大于三的卡要写留牌策略才能留下,费用小于三的卡要写留牌策略才能弃掉。

当然,以上内容都是一些基础的介绍,可以增强大家对留牌文件的理解
如果你对文章内容还是没有理解的话,我推荐使用紫火大佬制作的HearthBuddy留牌编辑器来对留牌策略进行编写。

HearthBuddy留牌编辑器

下面贴出奥秘法的留牌策略方便学习理解

//奥秘法
//暗金教侍从
CFM_066;mage;None;Hold:1;/
CFM_066;mage;None;Hold:2;/GAME_005/LOOT_101/EX1_287/UNG_020/FP1_018/ULD_239
//肯瑞托法师
EX1_612;mage;None;Hold:1;/LOOT_101/EX1_287/UNG_020/FP1_018/ULD_239
//法术法制
EX1_287;mage;None;Hold:1;/CFM_066/EX1_612
EX1_287;mage;None;Discard:2;/
//寒冰护体
EX1_289;mage;None;Hold:1;/CFM_066/EX1_612
EX1_289;mage;None;Discard:2;/
//爆炸符文
LOOT_101;mage;None;Hold:1;/CFM_066/EX1_612
LOOT_101;mage;None;Discard:2;/
//寒冰屏障
EX1_295;mage;None;Discard:2;/
//火焰结界
ULD_239;mage;hunter;Hold:1;/CFM_066/EX1_612
ULD_239;mage;shaman;Hold:1;/CFM_066/EX1_612
ULD_239;mage;pala;Hold:1;/CFM_066/EX1_612
ULD_239;mage;warrior;Discard:2;/
ULD_239;mage;mage;Discard:2;/
ULD_239;mage;priest;Discard:2;/
ULD_239;mage;thief;Hold:1;/CFM_066/EX1_612
ULD_239;mage;warlock;Hold:1;/CFM_066/EX1_612
ULD_239;mage;druid;Discard:2;/
//复制
FP1_018;mage;None;Hold:1;/EX1_612
FP1_018;mage;None;Discard:2;/
//远古谜团
ULD_726;mage;None;Discard:2;/CFM_066
//麦迪文的男仆
KAR_092;mage;None;Hold:1;/FP1_004/
//老旧的火把
LOE_002;mage;None;Discard:2;/
//对空奥术法师
ULD_240;mage;None;Discard:2;/
ULD_240;mage;warlock;Hold:1;/ULD_726
ULD_240;mage;thief;Hold:1;/ULD_726
ULD_240;mage;pala;Hold:1;/ULD_726
ULD_240;mage;shaman;Hold:1;/ULD_726
ULD_240;mage;hunter;Hold:1;/ULD_726
//奥术增幅体
YOD_008;mage;None;Discard:2;/

这一块到这里就结束啦,如果有疑问请评论。

_settings.txt(AI配置)

_settings.txt是放在策略目录下的文件,如果文件夹下不存在此文件则需要自己创建一个。
如文件位置:Routines\DefaultRoutine\Silverfish\behavior\策略名称\_settings.txt

先看一下文件内容

// If you want to set your own settings, rename this file to _settings.txt and place it in the target folder.

enfacehp = 15;  // hp of enemy when your hero is allowed to attack the enemy face with his weapon without penalty
// weaponOnlyAttackMobsUntilEnfacehp - If your opponent has more HP than enfacehp, then weapons are allowed only attack mobs
// 0 - don't attack face until enfacehp (except weapons with 1 Attack)
// 1 - don't attack face until enfacehp if weapon's durability = 1 (if durability > 1 then it's allowed)(except weapons with 1 Attack)
// 2 - don't attack face until enfacehp (any weapon)
// 3 - don't attack face until enfacehp if weapon's durability = 1 (if durability > 1 then it's allowed)(any weapon)
// 4 - don't attack face until enfacehp (except: you have any* weapon generating card in hand)(* except Upgrade!)
// 5 - don't attack face until enfacehp (except: you have any* weapon generating card in hand with attack > 1 (or if they both have attack = 1))(* except Upgrade!)
weaponOnlyAttackMobsUntilEnfacehp = 0;

maxwide = 3000;   // number of boards which are taken to the next deep-lvl
playaround = false;  // play around some enemys aoe-spells
// these two parameters are value between 0 and 100 (0 <= Your_Value <= 100)
playaroundprob = 50;    // probability where the enemy NOT plays the aoe-spell: 100 - enemy never plays aoe-spell, 0 - always uses
playaroundprob2 = 80;   // probability where the enemy plays the aoe-spell, and your minions will survive: 100 - always survive, 0 - never(survival depends on their real HP)
            
twotsamount = 0; // number of boards where the second AI step is simulated
enemyTurnMaxWide = 40; // max number of enemy boards calculated in enemys-first-turn first AI step (lower than enemyTurnMaxWideSecondStep)
enemyTurnMaxWideSecondStep = 200; // max number of enemy boards calculated in enemys-first-turn second AI step(higher than enemyTurnMaxWide)
            
nextTurnDeep = 10; //maximum actions in your second turn
nextTurnMaxWide = 20; //maximum best boards for calculation at each step in the second round
nextTurnTotalBoards = 200;//maximum boards calculated in second turn simulation
berserkIfCanFinishNextTour = 1; // 0 - off(default), 1 - if there is any chance to kill the enemy through the round, all attacks will be in the face

alpha = 50; // weight of the second turn in calculation (0<= alpha <= 100)
useSecretsPlayAround = true; // playing arround enemys secrets
placement = 0;  // 0 - minions are interleaved by value (..low value - hi value..), 1 - hi val minions along the edges, low val in the center

ImprovedCalculations = 1;   // 0 - disabled(for old PCs), 1 - enabled
adjustActions = 0; // test!! - reorder actions after calculations: 0 - as calculated (by Default), 1 - AoE first
printRules = 1; //0 - off, 1 - on

接下来对每个配置进行说明

参数名含义
enfacehp如果对面英雄的血量低于设定的值,就优先打脸。比如这里设定的值是15
weaponOnlyAttackMobsUntilEnfacehp这里有六个设定值,前置条件都是对面血量大于enfacehp
0: 除了一攻武器,其他的不打脸
1:一攻武器和武器耐久大于1时打脸
2:任何武器都不打脸
3:武器耐久度大于1时打脸
4:有其他武器在手的时候打脸(升级 算作一攻武器)
5:有其他一攻以上的武器在手或者手上的和装备着的都是一攻武器的时候攻击(升级 算作一攻武器)
maxwide最大的搜索宽度,详情见Ai.cs解析
playaround设置值为true之后会防御AOE技能
playaroundprob设置范围(0 - 100)
表示对手有多少几率会不使用AOE法术
0:总是使用 100:一定不会使用
playaroundprob2设置范围(0 - 100)
表示对手有多少几率使用AOE法术后你的随从会存活
0:不会存活 ,100:总是会存活
twotsamount模拟多少回合之后的操作
比如设置为1,就会模拟你一回合之后的情况
enemyTurnMaxWide兄弟不仅会模拟自己的操作,也会模拟对面的操作,类似于maxwide
要求大于enemyTurnMaxWideSecondStep
enemyTurnMaxWideSecondStep在敌人回合的第二次操作中,最多计算多少步
要求小于enemyTurnMaxWide
nextTurnDeep下一回合的深度,详情见Ai.cs解析
nextTurnMaxWide下一回合的最大宽度,详情见Ai.cs解析
nextTurnTotalBoards下一回合最多的场面记录数
berserkIfCanFinishNextTour填入1时,如果当前场面全部打脸,下一回合就可以斩杀,这一回合就会全部打脸
填入0则不开启
alpha设置范围(0 - 100)在Ai.cs中提到过的分配权重,分配给当前回合模拟和下一回合模拟的权重
比如这里填0,则就是下一回合的信息不计入计算
useSecretsPlayAround开启之后可以防奥秘,无脑抢血的卡组建议关掉
placement随从放置位置设定
为0时随从放置会根据其价值交错开来(低价值 - 高价值 - 低价值)
为1时高价值的在两侧,低价值的在中间
ImprovedCalculations提高计算能力
老电脑填0,新电脑填1
adjustActions对操作重排
填1时优先使用AOE攻击 重排函数在:ActionNormalizer.cs中
printRules填1:输出规则 填0:不输出

_combo.txt(连招配置)

_combo.txt是放在策略目录下的文件,如果文件夹下不存在此文件则需要自己创建一个。
如文件位置:Routines\DefaultRoutine\Silverfish\behavior\策略名称\_combo.txt

文件格式:

卡牌1ID,卡牌1惩罚;卡牌2ID,卡牌2惩罚;[附加参数]

附加参数:

参数名含义
manaCombo所需的法力值,兄弟会自动修正
bonus优先级,越高优先级越大
nxttrn是否为两回合Combo
bonusfirst第一张卡的优先级
bonussecond第二张卡的优先级

参数的添加方法一般是:参数名:参数值
且参数之间用 冒号 隔开,只有 nxttrn 不需要冒号和参数值

职业信息:

职业简称含义
druid德鲁伊
hunter猎人
mage法师
pala圣骑士
priest牧师
thief盗贼
shaman萨满
warlock术士
warrior战士
demonhunter恶魔猎手
不填hero参数任意职业

给出一些例子便于理解:

//先使用水晶学再使用风驰电掣,构成一套Combo
BOT_909,0;CFM_305,0;mana:2;bonus:100;hero:pala;
//本回合使用暴走旋风,下回合使用奥术暴龙,构成一套Combo
DAL_742,0;TRL_311,0;mana:14;nxttrn;bonus:100;

这一块到这里就结束啦,如果有疑问请评论。

_rules.txt(规则引擎)

规则引擎的编写相当复杂,先等我琢磨透了再来写吧~

Behavior控场模式.cs

对自带策略的解说

Behavior怼脸模式.cs

对自带策略的解说

SIM事件和常用函数整理
首先要学会在头部引用三个库文件,这样可以方便后期的整理与修改

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

奥秘触发事件(onSecretPlay)

onSecretPlay有三张不同参数的写法

public virtual void onSecretPlay(Playfield p, bool ownplay, Minion attacker, Minion target, out int number)
public virtual void onSecretPlay(Playfield p, bool ownplay, Minion target, int number)
public virtual void onSecretPlay(Playfield p, bool ownplay, int number)

我分别给出例子

自动防御矩阵

//<b>奥秘:</b>当你的随从受到攻击时,使其获得<b>圣盾</b>。
public override void onSecretPlay(Playfield p, bool ownplay, Minion attacker, Minion target, out int number)
{
    number = 0;
    if (ownplay)
    {
        if (p.ownMinions.Count >= 1)
        {
            if (p.ownMinions[p.ownMinions.Count - 1].name == CardDB.cardName.defender)
            {
                target.divineshild = true;
            }
        }
    }
    else
    {
        if (p.enemyMinions.Count >= 1)
        {
            if (p.enemyMinions[p.enemyMinions.Count - 1].name == CardDB.cardName.defender)
            {
                target.divineshild = true;
            }
        }
    }
}

爆炸符文

//<b>奥秘:</b>在你的对手使用一张随从牌后,对该随从造成$6点伤害,超过其生命值上限的伤害将由对方英雄承受。
public override void onSecretPlay(Playfield p, bool ownplay, Minion target, int number)
{
    int dmg = (ownplay) ? p.getSpellDamageDamage(6) : p.getEnemySpellDamageDamage(6);
    if (target != null)
    {
        if (target.Hp < dmg)
            p.minionGetDamageOrHeal((ownplay) ? p.enemyHero : p.ownHero, dmg - target.Hp);
        p.minionGetDamageOrHeal(target, dmg);
    }            
}

火焰结界

//<b>奥秘:</b>在一个随从攻击你的英雄后,对所有敌方随从造成$3点伤害。
public override void onSecretPlay(Playfield p, bool ownplay, int number)
{
    int dmg = (ownplay) ? p.getSpellDamageDamage(3) : p.getEnemySpellDamageDamage(3);
    p.allMinionOfASideGetDamage(!ownplay, dmg);
}    

使用法术牌事件(onCardPlay)

奥术智慧

//抽两张牌。
public override void onCardPlay(Playfield p, bool ownplay, Minion target, int choice)
{
    p.drawACard(CardDB.cardIDEnum.None, ownplay);
    p.drawACard(CardDB.cardIDEnum.None, ownplay);
}

弃牌事件(onCardDicscard)

玛克扎尔的小鬼

//每当你弃掉一张牌时,抽一张牌。
public override bool onCardDicscard(Playfield p, Handmanager.Handcard hc, Minion own, int num, bool checkBonus)
{
    if (own == null) return false;
    if (checkBonus) return false;
    p.drawACard(CardDB.cardIDEnum.None, own.own);
    return false;
}

镀银魔像

//如果你弃掉了这张随从牌,则会召唤它。
public override bool onCardDicscard(Playfield p, Handmanager.Handcard hc, Minion own, int num, bool checkBonus)
{
    if (checkBonus) return true;
    if (own != null) return false;
    
    bool ownplay = true;
    List<Minion> temp = (ownplay) ? p.ownMinions : p.enemyMinions;
    p.callKid(hc.card, temp.Count, ownplay, false);
    Minion m = temp[temp.Count - 1];
    if (m.name == hc.card.name && m.playedThisTurn)
    {
        m.entitiyID = hc.entity;
        m.Angr += hc.addattack;
        m.Hp += hc.addHp;
    }
    return true;
}

稍微研究了一下弃牌函数。
一般来说这个函数返回true的时候降低弃牌的惩罚。
如果函数返回了false就不会降低,所以增益效果强的卡牌应当返回true。
checkBonus是用来检测卡牌是否有弃牌效果的,也就是检测并不模拟SIM效果。
own指的是弃掉随从卡的时候使用,或者场上随从相应的弃牌效果

战吼事件(getBattlecryEffect)

注意:炉石兄弟存在一个问题,武器牌的战吼并不能写在这个函数中,而是要写在法术牌的onCardPlay事件中,这可能是一个bug。
所以,用到这个事件的只能是随从的SIM

雏龙巨婴

//<b>战吼:</b>抽一张牌。
public override void getBattlecryEffect(Playfield p, Minion own, Minion target, int choice)
{
    p.drawACard(CardDB.cardName.unknown, own.own);
}

光环特效(onAuraStarts & onAuraEnds)

onAuraStartsonAuraEnds一般都是成对出现的
战歌指挥官

//你的具有冲锋的随从获得+1攻击力。
public override void onAuraStarts(Playfield p, Minion own)
{
    if (own.own)
    {
        foreach (Minion m in p.ownMinions)
        {
            if (m.charge > 0) p.minionGetBuffed(m, 1, 0);
        }
    }
    else
    {
        foreach (Minion m in p.enemyMinions)
        {
            if (m.charge > 0) p.minionGetBuffed(m, 1, 0);
        }
    }
    
}

public override void onAuraEnds(Playfield p, Minion own)
{
    if (own.own)
    {
        foreach (Minion m in p.ownMinions)
        {
            if (m.charge > 0) p.minionGetBuffed(m, -1, 0);
        }
    }
    else
    {
        foreach (Minion m in p.enemyMinions)
        {
            if (m.charge > 0) p.minionGetBuffed(m, -1, 0);
        }
    }
}

激怒特效(onEnrageStart & onEnrageStop)

onEnrageStartonEnrageStop一般都是成对出现的
牛头人战士

//受伤时具有+3攻击力。
public override void onEnrageStart(Playfield p, Minion m)
{
    m.Angr += 3;
}

public override void onEnrageStop(Playfield p, Minion m)
{
    m.Angr -= 3;
}

治疗事件(onAMinionGotHealedTrigger & onAHeroGotHealedTrigger & onACharGotHealed)

北郡牧师

//每当一个随从获得治疗时,抽一张牌。
public override void onAMinionGotHealedTrigger(Playfield p, Minion triggerEffectMinion, int minionsGotHealed)
{
    for (int i = 0; i < minionsGotHealed; i++)
    {
        p.drawACard(CardDB.cardIDEnum.None, triggerEffectMinion.own);
    }
}

黑色卫士

//每当你的英雄获得治疗时,便随机对一个敌方随从造成等量的伤害。
public override void onAHeroGotHealedTrigger(Playfield p, Minion triggerEffectMinion, bool ownerOfHeroGotHealed)
{
    int dmg = ownerOfHeroGotHealed;
    Minion target = null;
    if (triggerEffectMinion.own) target = p.getEnemyCharTargetForRandomSingleDamage(dmg, true);
    else target = p.searchRandomMinion(p.ownMinions, searchmode.searchHighestAttack); //damage the Highest (pessimistic)
    if (target != null) p.minionGetDamageOrHeal(target, dmg);
}

圣光护卫者

//每当一个角色获得治疗,便获得+2攻击力。
public override void onACharGotHealed(Playfield p, Minion triggerEffectMinion, int charsGotHealed)
{
    p.minionGetBuffed(triggerEffectMinion, 2 * charsGotHealed, 0);
}

回合事件(onTurnStartTrigger & onTurnEndsTrigger)

常用

if (turnStartOfOwner == triggerEffectMinion.own)

来判定是否在随从拥有者的回合开始或结束

末日预言者

//在你的回合开始时,消灭所有随从。
public override void onTurnStartTrigger(Playfield p, Minion triggerEffectMinion, bool turnStartOfOwner)
{
    if (turnStartOfOwner == triggerEffectMinion.own)
    {
        foreach (Minion m in p.ownMinions)
        {
            if (m.entitiyID == triggerEffectMinion.entitiyID) continue;
            if (m.playedThisTurn || m.playedPrevTurn)
            {
                if (PenalityManager.Instance.ownSummonFromDeathrattle.ContainsKey(m.name)) continue;
                p.evaluatePenality += (m.Hp + m.Angr) * 6;
            }
        }
        p.allMinionsGetDestroyed();
    }
}

迦顿男爵

//在你的回合结束时,对所有其他角色造成2点伤害。
public override void onTurnEndsTrigger(Playfield p, Minion triggerEffectMinion, bool turnEndOfOwner)
{
    if (turnEndOfOwner == triggerEffectMinion.own)
    {
        p.allCharsGetDamage(2, triggerEffectMinion.entitiyID);
    }
}

伤害事件(onMinionGotDmgTrigger)

苦痛侍僧

//每当该随从受到伤害,抽一张牌。
public override void onMinionGotDmgTrigger(Playfield p, Minion m, int anzOwnMinionsGotDmg, int anzEnemyMinionsGotDmg, int anzOwnHeroGotDmg, int anzEnemyHeroGotDmg)
{
    if (m.anzGotDmg > 0)
    {
        int tmp = m.anzGotDmg;
        m.anzGotDmg = 0;
        for (int i = 0; i < tmp; i++)
        {
            p.drawACard(CardDB.cardIDEnum.None, m.own);
        }
    }
}

死亡事件(onMinionDiedTrigger)

注意:死亡事件不等同于亡语事件,亡语事件请使用onDeathrattle
这里就要搬出一个教科书式的官方SIM
每召唤一个鱼人就获得+1攻击力,每有一个鱼人死亡就-1攻击力
老瞎眼

//冲锋,在战场上每有一个其他鱼人便获得+1攻击力。
public override void getBattlecryEffect(Playfield p, Minion own, Minion target, int choice)
{
    foreach (Minion m in p.ownMinions)
    {
        if (m.handcard.card.race == 14)
        {
            if (m.entitiyID != own.entitiyID) p.minionGetBuffed(own, 1, 0);
        }
    }

    foreach (Minion m in p.enemyMinions)
    {
        if (m.handcard.card.race == 14)
        {
            if (m.entitiyID != own.entitiyID) p.minionGetBuffed(own, 1, 0);
        }
    }
}

public override void onMinionIsSummoned(Playfield p, Minion triggerEffectMinion, Minion summonedMinion)
{
    if (summonedMinion.handcard.card.race == 14)
    {
        p.minionGetBuffed(triggerEffectMinion, 1, 0);
    }
}

public override void onMinionDiedTrigger(Playfield p, Minion m, Minion diedMinion)
{
    int diedMinions = p.tempTrigger.ownMurlocDied + p.tempTrigger.enemyMurlocDied;
    if (diedMinions == 0) return;
    int residual = (p.pID == m.pID) ? diedMinions - m.extraParam2 : diedMinions;
    m.pID = p.pID;
    m.extraParam2 = diedMinions;
    if (residual >= 1)
    {
        p.minionGetBuffed(m, -1 * residual, 0);
    }
}

召唤事件(onMinionIsSummoned & onMinionWasSummoned)

onMinionIsSummonedonMinionWasSummoned的区别在于前者是随从准备被召唤了,但是还没有存在于场面上。后者是随从已经被召唤到场面上了。
下面大胖的这个例子就只能用onMinionWasSummoned,原因是它需要对所召唤的随从进行buff,而用onMinionIsSummoned并不能在p.ownMinions中读取到需要增幅的随从
公正之剑

//在你召唤一个随从后,使其获得+1/+1,这把武器失去1点耐久度。
public override void onCardPlay(Playfield p, bool ownplay, Minion target, int choice)
{
    p.equipWeapon(card,ownplay);
}

public override void onMinionIsSummoned(Playfield p, Minion triggerEffectMinion, Minion summonedMinion)
{
    if (triggerEffectMinion.own == summonedMinion.own )
    {
        p.minionGetBuffed(summonedMinion, 1, 1);
        p.lowerWeaponDurability(1, triggerEffectMinion.own);
    }
}

大胖

//每当你使用一张攻击力为1的随从牌,便使该牌所召唤的随从获得+2/+2。
public override void onMinionWasSummoned(Playfield p, Minion m, Minion summonedMinion)
{
    if (summonedMinion.playedFromHand && summonedMinion.Angr == 1 && m.own == summonedMinion.own && m.entitiyID != summonedMinion.entitiyID)
    {
        p.minionGetBuffed(summonedMinion, 2, 2);
    }
}

亡语事件(onDeathrattle)

麻风侏儒

//亡语:对敌方英雄造成2点伤害。
public override void onDeathrattle(Playfield p, Minion m)
{
    p.minionGetDamageOrHeal(m.own ? p.enemyHero : p.ownHero, 2);
}

出牌事件(onCardIsGoingToBePlayed & onCardWasPlayed)

onCardIsGoingToBePlayedonCardWasPlayed的区别在于前者是牌准备打出的时候的事件,而后者是牌已经被打出的时候的事件。
onCardIsGoingToBePlayed分为两个不同参数的版本

public virtual void onCardIsGoingToBePlayed(Playfield p, Handmanager.Handcard hc, bool wasOwnCard, Minion triggerEffectMinion)
public virtual void onCardIsGoingToBePlayed(Playfield p, Handmanager.Handcard hc, bool wasOwnCard, Handmanager.Handcard triggerhc)

两者的区别比较大,简单的来说,前者写的一般是你场上随从发生的事件,而后者一般写的是你手牌里发生的事件,话不多说,来看例子吧

任务达人

//每当你使用一张牌时,便获得+1/+1。
public override void onCardIsGoingToBePlayed(Playfield p, Handmanager.Handcard hc, bool wasOwnCard, Minion triggerEffectMinion)
{
    if (triggerEffectMinion.own == wasOwnCard)
    {
        p.minionGetBuffed(triggerEffectMinion, 1, 1);
    }
}

黑金大亨

//每当你召唤一个具有战吼的随从时,便使这张牌(在你手牌中时)获得+1/+1。
public override void onCardIsGoingToBePlayed(Playfield p, Handmanager.Handcard hc, bool wasOwnCard, Handmanager.Handcard triggerhc)
{
    if (hc.card.battlecry && hc.card.type == CardDB.cardtype.MOB)
    {
        hc.addattack++;
        hc.addHp++;
    }
}

onCardWasPlayed目前还没有合适的例子,这两者的区别类似于召唤事件的两个函数

激励事件(onInspire)

达拉然铁骑士

//激励:获得法术伤害+1。
public override void onInspire(Playfield p, Minion m, bool own)
{
    if (m.own == own)
    {
        m.spellpower++;
        if (m.own) p.spellpower++;
        else p.enemyspellpower++;
    }
}
        
public override void onAuraEnds(Playfield p, Minion m)
{
    if (m.own) p.spellpower -= m.spellpower;
    else p.enemyspellpower -= m.spellpower;
}

失去圣盾事件(onMinionLosesDivineShield)

浴火者伯瓦尔

//圣盾在一个友方随从失去圣盾后,获得+2攻击力。
public override void onMinionLosesDivineShield(Playfield p, Minion m, int num)
{
    p.minionGetBuffed(m, 2 * num, 0);
}

发现牌的权值(getDiscoverScore)

注意:这个SIM函数是我后期加上去的,在原版兄弟中并没有这个函数,添加的方法请见贴吧,后期我也会在博客里发出来
public virtual int getDiscoverScore(Playfield p)
返回增加的权值,权值越大越容易被选择。
红龙女王阿莱克丝塔萨

//红龙女王是一张非常超模的卡,9费8-8还送两张0费龙卡,基本上宇宙体系看到就必选,但不是宇宙体系则不需要
//于是我们可以在这个函数中对牌库中是否为宇宙体系进行判断,从而来驱使兄弟是否选择这张牌。
public override int getDiscoverScore(Playfield p)
{
    if (p.prozis.noDuplicates) return 500;
    else return 0;
}

以下所有函数都是在Playfield.cs中,在SIM中使用时请用p.xxxx

计算我方/敌方英雄英雄技能伤害

public int getHeroPowerDamage(int dmg)
public int getEnemyHeroPowerDamage(int dmg)

计算我方/敌方法术伤害

public int getSpellDamageDamage(int dmg)
public int getEnemySpellDamageDamage(int dmg)
//常用写法
int dmg = (ownplay) ? p.getSpellDamageDamage(3) : p.getEnemySpellDamageDamage(3);    

计算我方/敌方法术治疗效果

public int getSpellHeal(int heal)
public int getEnemySpellHeal(int heal)

计算我方/敌方随从治疗效果

public int getMinionHeal(int heal)
public int getEnemyMinionHeal(int heal)

使用法术吸血

public void applySpellLifesteal(int heal, bool own)

随从攻击随从(第三个参数:背叛效果)

public void minionAttacksMinion(Minion attacker, Minion defender, bool dontcount = false)

用武器攻击(英雄攻击)

public void attackWithWeapon(Minion hero, Minion target, int penality)

我方/敌方出牌

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

降低武器耐久

public void lowerWeaponDurability(int value, bool own)

获得获取清除所有Buff

public void minionGetOrEraseAllAreaBuffs(Minion m, bool get)

装备武器

public void equipWeapon(CardDB.Card c, bool own)

召唤随从

public void callKid(CardDB.Card c, int zonepos, bool own, bool spawnKid = true, bool oneMoreIsAllowed = false)

设置随从信息

//冻结一个随从
public void minionGetFrozen(Minion target)
//沉默一个随从
public void minionGetSilenced(Minion m)
//消灭一个随从
public void minionGetDestroyed(Minion m)
//设置随从控制方
public void minionGetControlled(Minion m, bool newOwner, bool canAttack, bool forced = false)
//设置随从磁力
public void Magnetic(Minion mOwn)
//设置随从风怒
public void minionGetWindfurry(Minion m)
//设置随从冲锋
public void minionGetCharge(Minion m)
//设置随从突袭
public void minionGetRush(Minion m)
//设置随从丢失突袭
public void minionLostCharge(Minion m)
//设置随从丢失圣盾
public void minionLosesDivineShield(Minion m)
//设置所有随从被消灭
public void allMinionsGetDestroyed()

Buff相关(攻击力,血量,护甲)

//获得一回合Buff(下回合消失)
public void minionGetTempBuff(Minion m, int tempAttack, int tempHp)
//设置一方全部随从获得Buff
public void allMinionOfASideGetBuffed(bool own, int attackbuff, int hpbuff)
//设置随从获得Buff
public void minionGetBuffed(Minion m, int attackbuff, int hpbuff)
//设置随从获得相邻buff
public void minionGetAdjacentBuff(Minion m, int angr, int vert)
//设置克苏恩获得Buff
public void cthunGetBuffed(int attackbuff, int hpbuff, int tauntbuff)
//设置攻击力为X
public void minionSetAngrToX(Minion m, int newAngr)
//设置血量为X
public void minionSetLifetoX(Minion m, int newHp)
//设置英雄获得护甲
public void minionGetArmor(Minion m, int armor)
//设置随从的攻击力为血量
public void minionSetAngrToHP(Minion m)
//交换随从的攻击力和血量
public void minionSwapAngrAndHP(Minion m)

受到伤害或者治疗:

//对随从受到伤害或者治疗
public void minionGetDamageOrHeal(Minion m, int dmgOrHeal, bool dontDmgLoss = false)
//对一方全部随从受到伤害或者治疗
public void allMinionOfASideGetDamage(bool own, int damages, bool frozen = false)
//对一方全部成员(随从和英雄)受到伤害或者治疗
public void allCharsOfASideGetDamage(bool own, int damages)
//对一方全部成员(随从和英雄)受到【随机】伤害或者治疗(造成的伤害随机分配给所有敌人)
public void allCharsOfASideGetRandomDamage(bool ownSide, int times) 
//对全部成员(随从和英雄)受到伤害或者治疗(除exceptID外,传入m.entitiyID)
public void allCharsGetDamage(int damages, int exceptID = -1)
//对全部随从受到伤害或者治疗(除exceptID外,传入m.entitiyID)
public void allMinionsGetDamage(int damages, int exceptID = -1)

弃牌(num:弃掉的牌数)

public void discardCards(int num, bool own)

设置一个新的英雄技能,英雄卡用

public void setNewHeroPower(CardDB.cardIDEnum newHeroPower, bool own)

摸一张牌

public void drawACard(CardDB.cardName ss, bool own, bool nopen = false)
public void drawACard(CardDB.cardIDEnum ss, bool own, bool nopen = false)

移除卡牌

public void removeCard(Handmanager.Handcard hcc)

将随从移回手牌(第三个参数:费用变化,降费填负数)

public void minionReturnToHand(Minion m, bool own, int manachange)
//示例:冰冻陷阱
//奥秘:当一个敌方随从攻击时,将其移回拥有者的手牌,并且法力值消耗增加(2)点。
public override void onSecretPlay(Playfield p, bool ownplay, Minion target, int number)
{
    p.minionReturnToHand(target, !ownplay, 2);
    target.Hp = -100;
}

随从移回到牌库

public void minionReturnToDeck(Minion m, bool own)

使随从变形成为另一个(例如:将所有随从转为传说随从)

public void minionTransform(Minion m, CardDB.Card c)

搜索随从

//随机搜索一个敌方目标造成单次伤害(参数二标记是否只为随从)
public Minion getEnemyCharTargetForRandomSingleDamage(int damage, bool onlyMinions = false)
//搜索随从(searchmode:搜索模式)
public Minion searchRandomMinion(List<Minion> minions, searchmode mode)

得到一张随机(限定法力值)的卡牌(例如:退化)

public CardDB.Card getRandomCardForManaMinion(int manaCost)

常用的一些成员

//超载
p.ueberladung

//法术伤害
p.spellpower
p.enemyspellpower

//我方/敌方英雄
p.ownHero
p.enemyHero

//我方/敌方随从
p.ownMinions
p.enemyMinions
//待补充...

SIM模版

//召唤一个随从
CardDB.Card kid = CardDB.Instance.getCardDataFromID(CardDB.cardIDEnum.UNG_201t); //这行代码建议写在重构函数的外面
int pos = ownplay ? p.ownMinions.Count : p.enemyMinions.Count;
p.callKid(kid, pos, own.own);

//卡牌数量
int cardsCount = (own.own) ? p.enemyAnzCards : p.owncards.Count;

//随从信息
List<Minion> temp = (own.own) ? p.enemyMinions : p.ownMinions;

//判定某张卡的种族
if((TAG_RACE)m.handcard.card.race == TAG_RACE.DRAGON)

//抓一张随机的卡
p.drawACard(CardDB.cardName.unknown, own.own);

//判定手牌有龙牌时做...
foreach (Handmanager.Handcard hc in p.owncards)
{
    if ((TAG_RACE)hc.card.race == TAG_RACE.DRAGON)
    {
        //所做的事件
        break;
    }
}

//随从的法术强度增加模版 (+1)
public override void onAuraStarts(Playfield p, Minion m)
{
    if (m.own) p.spellpower += 1;
    else p.enemyspellpower += 1;
}

public override void onAuraEnds(Playfield p, Minion m)
{
    if (m.own) p.spellpower -= 1;
    else p.enemyspellpower -= 1;
}

SIM书写常见问题
buffdebuff特效优化

//写在这个函数,速度更快一些
public void updateAdjacentBuffs(bool own)中

使用target参数时

//一定要在使用前先判定不为null
if(target != null)

使用搜索随从后

//使用getEnemyCharTargetForRandomSingleDamage
//或searchRandomMinion之后,一定要验证返回的Minion
//判定不为null之后再调用
if(m != null)

CardDefs.xml(卡牌数据)

XML文件的版本的卡牌数据,是非常容易理解的,只要看Tagname属性就能够理解对应的意义,所以在这里就不进行讲解了。
这里要讲的是的PlayRequirement的填写和意义
PlayRequirement的添加位置:一张卡的</Entity>前面。
目标只能是随从(沟渠潜伏者 - 战吼:消灭一个随从。亡语:再次召唤被消灭的随从。)

//REQ_MINION_TARGET
<PlayRequirement param="" reqID="1"/>

目标只能是友方(小型法术蓝宝石 - 选择一个友方随从,召唤一个它的复制)

//REQ_FRIENDLY_TARGET
<PlayRequirement param="" reqID="2"/>

目标只能是敌方 (战槌挑战者 - ;战吼:选择一个敌方随从。与其战斗至死!)

//REQ_ENEMY_TARGET
<PlayRequirement param="" reqID="3"/>

目标只能是受伤的随从(斩杀 - 消灭一个受伤的敌方随从。)

//REQ_DAMAGED_TARGET
<PlayRequirement param="" reqID="4"/>

目标只能是被冻结的随从(冰爆 - 消灭一个被冻结的随从。)

//REQ_FROZEN_TARGET
<PlayRequirement param="" reqID="6"/>

目标攻击力要求小于(暗言术:痛 - 消灭一个攻击力小于或等于3的随从。)

//REQ_TARGET_MAX_ATTACK
<PlayRequirement param="[攻击力]" reqID="8"/>

目标是除了自身英雄以外的目标(破法者 - 战吼:沉默一个随从。)

//REQ_NONSELF_TARGET
<PlayRequirement param="" reqID="9"/>

目标只能是指定种族的(牺牲契约 - 牺牲一个恶魔,为你的英雄恢复#5点生命值。)

//REQ_TARGET_WITH_RACE
<PlayRequirement param="[填种族数字]" reqID="10"/>

需要有目标

//REQ_TARGET_TO_PLAY
//注意,有部分随从是**只能以随从为目标**的,这类随从可能是可以空场下场的,记得把这类随从标记**reqID="22"**
<PlayRequirement param="" reqID="11"/>

场上可以放的随从数目(召唤守卫 - 召唤两个2/4的守卫。)

//REQ_NUM_MINION_SLOTS
//注意,如果是要召唤两个随从的,数目也是填1,因为两个随从只会出现一个,但是卡牌是可以使用的
<PlayRequirement param="[填数目]" reqID="12"/>

需要武器才能使用(吸血药膏 - 使你的武器获得吸血。)

//REQ_WEAPON_EQUIPPED
<PlayRequirement param="" reqID="13"/>

目标只能是英雄(阿莱克丝塔萨 - 战吼:将一方英雄的剩余生命值变为15。)

//REQ_HERO_TARGET
<PlayRequirement param="" reqID="17"/>

无目标时也可以使用(如:无面腐蚀者)

//REQ_TARGET_IF_AVAILABLE
<PlayRequirement param="" reqID="22"/>

敌方随从最少需要X个才能使用(关门放狗 - 战场上每有一个敌方随从,便召唤一个1/1并具有冲锋的猎犬。)

//REQ_MINIMUM_ENEMY_MINIONS
//注意,像是关门放狗这张牌数目就是填1
<PlayRequirement param="[填数目]" reqID="23"/>

连击时有目标(幽暗城勇士 - 连击:造成1点伤害。)

//REQ_TARGET_FOR_COMBO
<PlayRequirement param="" reqID="24"/>

要求目标攻击力大于(暗言术:灭 - 消灭一个攻击力大于或等于5的随从。)

//REQ_TARGET_MIN_ATTACK
<PlayRequirement param="[攻击力]" reqID="41"/>

对面场上的全部随从最少需要X个(弹射之刃 - 随机对一个随从造成$1点伤害。重复此效果,直到某个随从死亡。)

//REQ_MINIMUM_TOTAL_MINIONS
<PlayRequirement param="[填数目]" reqID="45"/>

目标必须是嘲讽随从(破盾者 - 战吼:沉默一个具有嘲讽的敌方随从。)

//REQ_MUST_TARGET_TAUNTER
<PlayRequirement param="" reqID="46"/>

目标只能是未受伤的随从(暗影打击 - 对一个未受伤的角色造成$5点伤害。)

//REQ_UNDAMAGED_TARGET
<PlayRequirement param="" reqID="47"/>

英雄技能(召唤伙伴 - 英雄技能:随机召唤一个动物伙伴。)

//REQ_MINION_OR_ENEMY_HERO
<PlayRequirement param="" reqID="50"/>

手牌中有龙牌的话则有目标(看台喷火龙 - 战吼:如果你的手牌中有龙牌,则对一个敌方随从造成7点伤害。)

//REQ_TARGET_IF_AVAILABLE_AND_DRAGON_IN_HAND
<PlayRequirement param="" reqID="51"/>

目标只能是传说随从(盖斯 - 消灭一个传说随从)

//REQ_LEGENDARY_TARGET
<PlayRequirement param="" reqID="52"/>

敌方有武器时可以使用(回响之锣 - 摧毁你对手的武器。)

//REQ_ENEMY_WEAPON_EQUIPPED
<PlayRequirement param="" reqID="55"/>

我方随从最少要X个可以使用(穿刺者戈莫克 - 战吼:如果你拥有至少四个其他随从,则造成4点伤害。)

//REQ_TARGET_IF_AVAILABLE_AND_MINIMUM_FRIENDLY_MINIONS
<PlayRequirement param="" reqID="56"/>

目标只能是亡语随从(哈霍兰公主 - 战吼:触发一个友方随从的亡语。)

//REQ_TARGET_WITH_DEATHRATTLE
<PlayRequirement param="" reqID="58"/>

最少需要X个奥秘才有目标(麦迪文的男仆 - 战吼:如果你控制一个奥秘则造成3点伤害。)

//REQ_TARGET_IF_AVAILABLE_AND_MINIMUM_FRIENDLY_SECRETS
<PlayRequirement param="[奥秘个数]" reqID="59"/>

目标只能是潜行随从(暗影大师 - 战吼:使一个潜行的随从获得+2/+2。)

//REQ_STEALTHED_TARGET
<PlayRequirement param="" reqID="62"/>

场上可以放一个随从且小于10个水晶:

//REQ_MINION_SLOT_OR_MANA_CRYSTAL_SLOT
<PlayRequirement param="" reqID="63"/>

如果上个回合打过元素牌则有目标(火焰使者 - 战吼:如果你在上个回合使用过元素牌,则造成5点伤害。)

//REQ_TARGET_IF_AVAILABE_AND_ELEMENTAL_PLAYED_LAST_TURN
<PlayRequirement param="" reqID="65"/>

必须先使用其他卡牌(暗影映像 - 每当你使用一张牌,变形成为该卡牌的复制。)

//REQ_MUST_PLAY_OTHER_CARD_FIRST
<PlayRequirement param="" reqID="69"/>

手牌未满(合成僵尸兽 - 英雄技能:制造一个自定义的僵尸兽。)

//REQ_HAND_NOT_FULL
//常用于英雄技能可以发现卡牌至手牌上的操作
<PlayRequirement param="" reqID="70"/>

这就是兄弟支持的所有PlayRequirement了(除了两个不常用的)

大家学会了如何增加卡牌信息之后,希望可以尝试添加一下新卡的数据。
如果有人做好了,可以尝试着在评论区分享一下。

最后这里教大家一个快速搜索出需要设定条件的卡牌的方法。

"手牌中有龙牌"的全部卡牌

附带:云雾王子的基础数据

<Entity CardID="ULD_293" ID="54493" version="2">
    <MasterPower>00000012-be43-4d77-8780-0d77d38da392</MasterPower>
    <Tag enumID="185" name="CARDNAME" type="LocString">
        <deDE>Wolkenprinz</deDE>
        <enUS>Cloud Prince</enUS>
        <esES>Príncipe de las Nubes</esES>
        <esMX>Príncipe de las nubes</esMX>
        <frFR>Prince-nuage</frFR>
        <itIT>Principe delle Nubi</itIT>
        <jaJP>雲の公子</jaJP>
        <koKR>구름 왕자</koKR>
        <plPL>Książę chmur</plPL>
        <ptBR>Príncipe das Nuvens</ptBR>
        <ruRU>Принц облаков</ruRU>
        <thTH>เจ้าชายเมฆ</thTH>
        <zhCN>云雾王子</zhCN>
        <zhTW>雲霧親王</zhTW>
    </Tag>
    <Tag enumID="184" name="CARDTEXT" type="LocString">
        <deDE>[x]&lt;b&gt;Kampfschrei:&lt;/b&gt;
Verursacht 6 Schaden,
wenn Ihr ein &lt;b&gt;Geheimnis&lt;/b&gt;
kontrolliert.</deDE>
        <enUS>&lt;b&gt;Battlecry:&lt;/b&gt; If you control a &lt;b&gt;Secret&lt;/b&gt;, deal 6 damage.</enUS>
        <esES>[x]&lt;b&gt;Grito de batalla:&lt;/b&gt;
Si controlas un &lt;b&gt;secreto&lt;/b&gt;,
inflige 6 p. de daño.</esES>
        <esMX>&lt;b&gt;Grito de batalla:&lt;/b&gt; si controlas un &lt;b&gt;Secreto&lt;/b&gt;, inflige 6 de daño.</esMX>
        <frFR>&lt;b&gt;Cri de guerre :&lt;/b&gt; si vous contrôlez un &lt;b&gt;Secret&lt;/b&gt;, inflige 6_points de dégâts.</frFR>
        <itIT>[x]&lt;b&gt;Grido di Battaglia:&lt;/b&gt;
infligge 6 danni se
controlli un &lt;b&gt;Segreto&lt;/b&gt;.</itIT>
        <jaJP>[x]&lt;b&gt;雄叫び:&lt;/b&gt;
自分の&lt;b&gt;秘策&lt;/b&gt;が準備
されている場合
_____6ダメージを与える。_</jaJP>
        <koKR>[x]&lt;b&gt;전투의 함성:&lt;/b&gt;
내 전장에 &lt;b&gt;비밀&lt;/b&gt;이 있으면,
피해를 6 줍니다.</koKR>
        <plPL>&lt;b&gt;Okrzyk bojowy:&lt;/b&gt;
Zadaj 6 pkt. obrażeń, jeśli kontrolujesz &lt;b&gt;Sekret&lt;/b&gt;.</plPL>
        <ptBR>&lt;b&gt;Grito de Guerra:&lt;/b&gt; Se você controlar um &lt;b&gt;Segredo&lt;/b&gt;, cause 6 de dano.</ptBR>
        <ruRU>&lt;b&gt;Боевой клич:&lt;/b&gt; если у вас есть активный &lt;b&gt;секрет&lt;/b&gt;,
[x]наносит 6 ед. урона.</ruRU>
        <thTH>&lt;b&gt;คำรามสู้ศึก:&lt;/b&gt;_ถ้าคุณมี &lt;b&gt;กับดัก&lt;/b&gt;_ในสนาม_สร้าง[b]ความเสียหาย_6_แต้ม</thTH>
        <zhCN>&lt;b&gt;战吼:&lt;/b&gt;
如果你控制一个&lt;b&gt;奥秘&lt;/b&gt;,则造成6点伤害。</zhCN>
        <zhTW>&lt;b&gt;戰吼:&lt;/b&gt;若你場上有
&lt;b&gt;秘密&lt;/b&gt;,造成6點傷害</zhTW>
    </Tag>
    <Tag enumID="351" name="FLAVORTEXT" type="LocString">
        <deDE>Ein gar luftiger Zeitgenosse, der sich aber reinhängt, dass die Funken nur so fliegen.</deDE>
        <enUS>&quot;In West Cloudidelphia, born and raised; flinging lightning for the rest of my days.&quot;</enUS>
        <esES>Al oeste en Nubedelfia crecía y vivía, sin hacerle mucho caso a los eremitas. Jugaba con rayos, sin cansarme demasiado, porque por las noches destruía algún poblado.</esES>
        <esMX>&quot;En Nimbusdelfia yo nací y crecí; con rayos que lanzar, fue una etapa feliz.&quot;</esMX>
        <frFR>Le nouveau prince-nuage, maintenant avec un fourrage à la vanille plus léger que jamais !</frFR>
        <itIT>&quot;Fulminando i miei nemici sono cresciuto, me la sono spassata, wow, che tuoni ogni minuto.&quot;</itIT>
        <jaJP>「くもプリ」と呼ばれ親しまれている彼は、その秘密めいたイケボでファンに6ダメージを与え昇天させている。</jaJP>
        <koKR>내 비밀이 하늘에 닿아 울려 구름도 나를 듣기까지, 마음에 들 때까지.</koKR>
        <plPL>Całe życie buja w obłokach.</plPL>
        <ptBR>Isso é que é vida de príncipe: pernas pra cima e a cabeça, só nas nuvens.</ptBR>
        <ruRU>Витает в облаках по долгу службы.</ruRU>
        <thTH>เกิดและโตบนท้องฟ้า เอาแต่ลอยไปลอยมาทั้งวัน</thTH>
        <zhCN>在那西方云雾之乡
我就在那地方成长
每天都要到处浪荡
还要放个闪电听响</zhCN>
        <zhTW>那個雲~那個霧啊~</zhTW>
    </Tag>
    <Tag enumID="325" name="TARGETING_ARROW_TEXT" type="LocString">
        <deDE>Verursacht 6 Schaden.</deDE>
        <enUS>Deal 6 damage.</enUS>
        <esES>Inflige 6 p. de daño.</esES>
        <esMX>Inflige 6 de daño.</esMX>
        <frFR>Inflige 6_points de dégâts.</frFR>
        <itIT>Infligge 6 danni.</itIT>
        <jaJP>6ダメージを与える。</jaJP>
        <koKR>피해 6</koKR>
        <plPL>Zadaj 6 pkt. obrażeń.</plPL>
        <ptBR>Cause 6 de dano.</ptBR>
        <ruRU>Нанести 6 ед. урона.</ruRU>
        <thTH>สร้างความเสียหาย_6_แต้ม</thTH>
        <zhCN>造成6点伤害。</zhCN>
        <zhTW>造成6點傷害</zhTW>
    </Tag>
    <Tag enumID="342" name="ARTISTNAME" type="String">Anton Zemskov</Tag>
    <Tag enumID="45" name="HEALTH" type="Int" value="4"/>
    <Tag enumID="47" name="ATK" type="Int" value="4"/>
    <Tag enumID="48" name="COST" type="Int" value="5"/>
    <Tag enumID="183" name="CARD_SET" type="Int" value="1158"/>
    <Tag enumID="199" name="CLASS" type="Int" value="4"/>
    <Tag enumID="200" name="CARDRACE" type="Int" value="18"/>
    <Tag enumID="202" name="CARDTYPE" type="Int" value="4"/>
    <Tag enumID="203" name="RARITY" type="Int" value="1"/>
    <Tag enumID="218" name="BATTLECRY" type="Int" value="1"/>
    <Tag enumID="321" name="COLLECTIBLE" type="Int" value="1"/>
    <ReferencedTag enumID="219" name="SECRET" type="Int" value="1"/>
    <Power definition="cabafd80-a4fc-474a-9b91-b9f36fe14d7d"/>
    <Power definition="00000012-be43-4d77-8780-0d77d38da392">
        <PlayRequirement param="1" reqID="59"/>
    </Power>
</Entity>

他这里用到的就是<PlayRequirement param="1" reqID="59"/>
注意:如果<Power definition="xxxxx">
这一行没有的话可以省略,兄弟也不读取。

<Tag enumID="342" name="ARTISTNAME" type="String">Anton Zemskov</Tag>
<Tag enumID="45" name="HEALTH" type="Int" value="4"/>
<Tag enumID="47" name="ATK" type="Int" value="4"/>
<Tag enumID="48" name="COST" type="Int" value="5"/>
<Tag enumID="183" name="CARD_SET" type="Int" value="1158"/>
<Tag enumID="199" name="CLASS" type="Int" value="4"/>
<Tag enumID="200" name="CARDRACE" type="Int" value="18"/>
<Tag enumID="202" name="CARDTYPE" type="Int" value="4"/>
<Tag enumID="203" name="RARITY" type="Int" value="1"/>
<Tag enumID="218" name="BATTLECRY" type="Int" value="1"/>
<Tag enumID="321" name="COLLECTIBLE" type="Int" value="1"/>
<ReferencedTag enumID="219" name="SECRET" type="Int" value="1"/>
<PlayRequirement param="1" reqID="59"/>

就像是这样也是可以的!

Last Modified: September 6, 2021
Archives QR Code Tip
QR Code for this page
Tipping QR Code