Skip to content

  • Projects
  • Groups
  • Snippets
  • Help
    • Loading...
    • Help
    • Support
    • Submit feedback
  • Sign in / Register
W
windbot
  • Project overview
    • Project overview
    • Details
    • Activity
    • Releases
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
    • Locked Files
  • Issues 0
    • Issues 0
    • List
    • Boards
    • Labels
    • Service Desk
    • Milestones
  • Merge Requests 0
    • Merge Requests 0
  • CI / CD
    • CI / CD
    • Pipelines
    • Jobs
    • Schedules
  • Security & Compliance
    • Security & Compliance
    • Dependency List
    • License Compliance
  • Packages
    • Packages
    • Container Registry
  • Analytics
    • Analytics
    • CI / CD
    • Code Review
    • Insights
    • Issues
    • Repository
    • Value Stream
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Members
    • Members
  • Collapse sidebar
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
  • MyCard
  • windbot
  • Wiki
  • 如何编写你自己的WindBot AI

如何编写你自己的WindBot AI

Last edited by Mercury233 May 29, 2018
Page history
This is an old version of this page. You can view the most recent version or browse the history.

WindBot用C#开发。C#比较易于使用,因此编写一个AI并不困难。本文以编写一个光道卡组为例,介绍编写WindBot AI的方法。

There will be an English version of this document.

准备工作

  • Visual Studio
    在VS2015上测试通过,VS2010理论上可用.
  • 基本的编程基础
    变量、函数、类、对象、数组、if、for、while之类的基础知识。
  • 基本的YGOPro知识
    不会打牌也想教AI打牌?

开始吧

1. 组一个卡组

image

(我不会玩光道,这是我乱组的)

卡组以易用为优先。一张卡的用法越多,写AI就越难。

把ydk文件命名为AI_Lightsworn,放到windbot的decks文件夹里。

2. 创建Executor

Executor(与Java的那个无关)是执行者的意思,用来给每个卡组规定每张卡片的用法等。

在Game\AI\Decks下新建代码文件,命名为LightswornExecutor。
(你操作时,LightswornExecutor可能已经被写好了,所以换个NewLightswornExecutor之类的名字)

在其中写以下代码:

using YGOSharp.OCGWrapper.Enums;
using System.Collections.Generic;
using WindBot;
using WindBot.Game;
using WindBot.Game.AI;

namespace WindBot.Game.AI.Decks
{
    [Deck("Lightsworn", "AI_Lightsworn")]
    public class LightswornExecutor : DefaultExecutor
    {

        public LightswornExecutor(GameAI ai, Duel duel)
            : base(ai, duel)
        {

        }

    }
}

可以看到,在WindBot.Game.AI.Decks下新建的LightswornExecutor继承了DefaultExecutor。
Deck属性的第一个参数是卡组名,第二个是卡组文件名。

3. 跑一下看看

在YGOPro中建立主机后,使用以下参数在VS中启动

Deck=Lightsworn

image

如果一切正常,你可以看到AI加入房间并且准备了。

  • 如果AI没有出现,请确定是先建立主机才启动的WindBot。
  • 如果AI不准备,请确定卡组文件放到了正确的位置,并且WindBot有正确的cards.cdb。

但是开始游戏后我们会发现AI什么牌都不会出,这是因为我们没有给AI指定卡的使用方法。

WindBot只会使用卡组的Executor中指定了的卡,其他卡一律不做任何操作,除了攻击和必发效果。

所以我们要做的事就是为每张卡写用法。

(当然dalao可以尝试写一个所有卡都会用的Executor)

4. 建立卡名类

为便于在代码中指定卡名,我们建立一个CardId类,把每个卡名写成常量。

在LightswornExecutor下(不是构造函数里)创建CardId类,定义卡组里的每张卡的ID:

        public class CardId
        {
            public const int JudgmentDragon = 57774843;
            public const int Wulf = 58996430;
            public const int Garoth = 59019082;
            public const int Raiden = 77558536;
            public const int Lyla = 22624373;
            public const int Felis = 73176465;
            public const int Lumina = 95503687;
            public const int Minerva = 40164421;
            public const int Ryko = 21502796;
            public const int PerformageTrickClown = 67696066;
            public const int Goblindbergh = 25259669;
            public const int ThousandBlades = 1833916;
            public const int Honest = 37742478;
            public const int GlowUpBulb = 67441435;

            public const int SolarRecharge = 691925;
            public const int GalaxyCyclone = 5133471;
            public const int HarpiesFeatherDuster = 18144506;
            public const int ReinforcementOfTheArmy = 32807846;
            public const int MetalfoesFusion = 73594093;
            public const int ChargeOfTheLightBrigade = 94886282;

            public const int Michael = 4779823;
            public const int MinervaTheExalted = 30100551;
            public const int TrishulaDragonOfTheIceBarrier = 52687916;
            public const int ScarlightRedDragonArchfiend = 80666118;
            public const int PSYFramelordOmega = 74586817;
            public const int PSYFramelordZeta = 37192109;
            public const int NumberS39UtopiatheLightning = 56832966;
            public const int Number39Utopia = 84013237;
            public const int CastelTheSkyblasterMusketeer = 82633039;
            public const int EvilswarmExcitonKnight = 46772449;
            public const int DanteTravelerOfTheBurningAbyss = 83531441;
            public const int DecodeTalker = 1861629;
            public const int MissusRadiant = 3987233;
        }

image

如果你感觉英文卡名看不懂,也可以用中文,不过这会导致其他人看不懂,自行权衡吧。

5. 最简单的Executor,让AI会发羽毛扫,会通招莱登

除了卡组的Executor,每张卡片都应该有一个或多个Executor。

下面我们在LightswornExecutor的构造函数里注册两个Executor:

AddExecutor(ExecutorType.Activate, CardId.HarpiesFeatherDuster);

这让AI在HarpiesFeatherDuster可以发动时发动它。

AddExecutor(ExecutorType.Summon, CardId.Raiden);

这让AI在Raiden可以通常召唤时召唤它。

image

在AI的主要阶段,手里同时有莱登和羽毛扫,如何决定先后顺序呢?

答案是根据注册Executor的顺序,先注册的先操作。

每当AI可以发动效果或召唤,它会检查当前的卡组的Executor,依次判断里面的操作是否可用,如果可用则进行操作。
然后会重新从头开始,因为YGOPro不支持一次进行2个操作,而是在进行完一个操作之后重新发给客户端可进行的操作的列表。

6. 稍微复杂一点,满足条件才发增援,并且选择指定的卡

现在我们看下一张卡,假如我们想要增援在手里没有莱登时拿莱登,没有小飞机时拿小飞机,都有则不发动,怎么办?

在LightswornExecutor下(不是构造函数里)创建一个函数:

        private bool ReinforcementOfTheArmyEffect()
        {
            if (!Bot.HasInHand(CardId.Raiden))
            {
                AI.SelectCard(CardId.Raiden);
                return true;
            }
            else if (!Bot.HasInHand(CardId.Goblindbergh))
            {
                AI.SelectCard(CardId.Goblindbergh);
                return true;
            }
            return false;
        }

然后在LightswornExecutor的构造函数里注册增援的Executor:

AddExecutor(ExecutorType.Activate, CardId.ReinforcementOfTheArmy, ReinforcementOfTheArmyEffect);

这样,当ReinforcementOfTheArmyEffect返回true时,就会发动增援了。

Bot是ClientField的一个实例,具有GetMonsters, HasInHand等方法。

AI.SelectCard的作用是预先选择卡片。在决定发动某个效果前调用,然后要选择卡片时就会按设定的目标选择了。

7. 使用默认的卡片Executor,以及一个技巧

DefaultExecutor里已经为很多卡片写了默认的Executor。因为我们的LightswornExecutor继承自它,我们可以直接使用。

比如银河旋风。

AddExecutor(ExecutorType.Activate, CardId.GalaxyCyclone, DefaultGalaxyCyclone);

DefaultGalaxyCyclone的功能是这卡在墓地时对方有表侧的魔陷就以它为对象发动,这卡在其他地方就以对方里侧的魔陷为对象发动。

现在有个小问题,如果手里同时有羽毛扫和银河旋风,对方盖了2张魔陷时,我们希望先发羽毛扫,但对方只有1张魔陷时就先发银河旋风,怎么办?

我们用的办法是为羽毛扫注册2个Executor,一个是当对方有2张以上魔陷时发动,一个是能发就发。

image

DefaultHarpiesFeatherDusterFirst就是对方有2张以上魔陷才发动。利用AI按顺序判断是否发动的特点,来达成我们的目的。

8. ExecutorType介绍

ExecutorType有如下几种:

  • Summon
    通常召唤,包括上级召唤
  • SpSummon
    特殊召唤,包括同调超量等,不包括仪式和融合,也不包括入连锁的特殊召唤。
  • Repos
    改变攻守
  • MonsterSet
    怪兽卡放置
  • SpellSet
    魔陷卡放置,包括古遗物等卡的作为魔陷放置
  • Activate
    发动,包括卡的发动和效果的发动,和诱发效果的发动
  • SummonOrSet
    对方怪兽比较强则放置,否则召唤

9. 一些“全局”变量

每个Executor设定的函数被执行前,都会将变量Card设置为当前被判断是否要发动或召唤的那张卡。

比如我们想让哥布林德伯格只在手里有这张卡以外的4星怪兽时才召唤:

        private bool GoblindberghSummon()
        {
            foreach (ClientCard card in Bot.Hand.GetMonsters())
            {
                if (!card.Equals(Card) && card.Level == 4)
                    return true;
            }
            return false;
        }

类似的变量还有:

  • ActivateDescription
    用于判断发动的是哪个效果,-1则为诱发效果(参考鸟铳士的DefaultCastelTheSkyblasterMusketeerEffect)
  • LastChainPlayer
    上次发动效果的玩家,0表示自己,1表示对面,-1表示没有人(针对不入连锁的召唤的神宣等)
  • Duel的Player, ChainTargets, LastSummonPlayer, LifePoints 等属性

10. 一次发动选择多次卡

对于带COST的效果,或者废铁龙之类的效果,我们需要在一次发动中进行多次选择。而AI.SelectCard只能设定一个选择。

这时我们可以使用AI.SelectNextCard和AI.SelectThirdCard来实现。

以露米娜丝的效果为例,我们想让她在墓地没有莱登时优先丢弃莱登,否则丢弃沃尔夫等卡,然后特殊召唤莱登等调整。

        private bool LuminaEffect()
        {
            if (!Bot.HasInGraveyard(CardId.Raiden) && Bot.HasInHand(CardId.Raiden))
            {
                AI.SelectCard(CardId.Raiden);
            }
            else
            {
                AI.SelectCard(new[] {
                    CardId.Wulf,
                    CardId.Felis,
                    CardId.Minerva,
                    CardId.ThousandBlades
                });
            }
            AI.SelectNextCard(new[] {
                    CardId.Raiden,
                    CardId.Felis
                });
            return true;
        }

当然,实际上的判断应该比这更详细,比如下面要讲到的小丑的判断……

11. 一张卡的多个效果

一张卡可能有多个效果,比如鸟铳士和光道圣女。这些效果可以被写到同一个Executor里,也可以写多个。

要判断发动的哪个效果,可以根据卡片的位置判断,同一个位置的多个效果可以根据效果的描述判断。 image

        private bool MinervaTheExaltedEffect()
        {
            if (Card.Location == CardLocation.MonsterZone)
            {
                return true;
            }
            else
            {
                IList<ClientCard> targets = new List<ClientCard>();

                ClientCard target1 = AI.Utils.GetBestEnemyMonster();
                if (target1 != null)
                    targets.Add(target1);
                ClientCard target2 = AI.Utils.GetBestEnemySpell();
                if (target2 != null)
                    targets.Add(target2);

                foreach (ClientCard target in Enemy.GetMonsters())
                {
                    if (targets.Count >= 3)
                        break;
                    if (!targets.Contains(target))
                        targets.Add(target);
                }
                foreach (ClientCard target in Enemy.GetSpells())
                {
                    if (targets.Count >= 3)
                        break;
                    if (!targets.Contains(target))
                        targets.Add(target);
                }
                if (targets.Count == 0)
                    return false;

                AI.SelectNextCard(targets);
                return true;
            }
        }

光道圣女如果是场上的效果,直接发动,否则预先选择要破坏的3张卡再发动。

        protected bool DefaultCastelTheSkyblasterMusketeerEffect()
        {
            if (ActivateDescription == AI.Utils.GetStringId(_CardId.CastelTheSkyblasterMusketeer, 0))
                return false;
            ClientCard target = AI.Utils.GetProblematicEnemyCard();
            if (target != null)
            {
                AI.SelectNextCard(target);
                return true;
            }
            return false;
        }

鸟铳士如果是第一个效果,不发动。第二个效果选择需要解决的对方的卡发动。

12. 重载OnNewTurn实现回合开始时重置的变量

如果这个回合没有使用过戏法小丑的效果,我们会想把它丢到墓地,但如果已经使用过,就不必了。

这时我们可以在LightswornExecutor下注册一个ClownUsed的变量,当判断小丑是否发动效果时将它设置成true再发动小丑,然后在相关卡发动效果时检查它。

bool ClownUsed = false;
        private bool PerformageTrickClownEffect()
        {
            ClownUsed = true;
            AI.SelectPosition(CardPosition.FaceUpDefence);
            return true;
        }

我们还需要在回合开始时重置ClownUsed。

Executor提供了OnNewTurn,默认回合开始时什么都不做,我们可以重载它。

        public override void OnNewTurn()
        {
            ClownUsed = false;
        }

这样,露米娜丝的判断就可以多一个else:

        private bool LuminaEffect()
        {
            if (!Bot.HasInGraveyard(CardId.Raiden) && Bot.HasInHand(CardId.Raiden))
            {
                AI.SelectCard(CardId.Raiden);
            }
            else if (!ClownUsed && Bot.HasInHand(CardId.PerformageTrickClown))
            {
                AI.SelectCard(CardId.PerformageTrickClown);
            }
            else
            {
...

13. 更多重载

除了OnNewTurn之外,Executor还有很多可以重载的方法。一些常用的如下:

  • OnSelectHand 猜拳赢时决定先后攻
  • OnSelectCard 处理复杂的选择卡片情况,参考青眼AI的龙觉醒旋律
  • OnSelectPendulumSummon 选择要被灵摆召唤的怪兽
  • OnPreBattleBetween 决定攻击是否不能进行

详细介绍一下OnPreBattleBetween。

AI判断能否攻击之前,会以自己的attacker和对方的defender为参数调用一次OnPreBattleBetween,如果返回值为false,则表示不能攻击。但OnPreBattleBetween不直接比较攻击力,而是更新RealPower,由AI的其他部分来比较RealPower。

所有AI继承的DefaultExecutor已经重载了OnPreBattleBetween,写了电光皇和水晶翼等常见卡的RealPower。

我们的光道卡组投入了欧尼斯特,所以需要再重载一次,更新RealPower。

        public override bool OnPreBattleBetween(ClientCard attacker, ClientCard defender)
        {
            if (!defender.IsMonsterHasPreventActivationEffectInBattle())
            {
                if (attacker.Attribute == (int)CardAttribute.Light && Bot.HasInHand(CardId.Honest))
                    attacker.RealPower = attacker.RealPower + defender.Attack;
            }
            return base.OnPreBattleBetween(attacker, defender);
        }

如果对方的defender不是电光皇等阻止欧尼斯特发动效果的卡,attacker是光属性,手里有欧尼斯特,就更新attacker的RealPower。然后返回被继承的OnPreBattleBetween继续处理其他情况。

14. 继续编写AI的一些提示

Clone repository
  • Home
  • Running Windbot
  • 如何编写你自己的WindBot AI
More Pages