Skip to content

How to create your WindBot AI

Mercury233 edited this page Apr 20, 2020 · 3 revisions

Chinese version: 如何编写你自己的WindBot AI

English version translated and wrote by AniHelp the Pure Wind, Szefo09, Sleeser, AntiMetaman


How to write your own WindBot AI

WindBot was developed in C#. C# is relatively easy to use, so writing an AI is not difficult. This article takes the preparation of an AI as an example to introduce the method of writing WindBot AI.

Requirements

  • Visual Studio confirmed to work on VS2019 and VS2015 (VS2010 is theoretically possible).

  • Basic programming knowledge such as variables, functions, classes, objects, arrays, if, for, and while.

  • Basic YGOPro knowledge

    • Knowledge of how to play the deck you wish to use will be required.
    • We will be teaching the AI how to use cards, so you will need a good knowledge of how each card in the deck works.
  • For local testing, using a YGOPro client with LAN mode and latest OCGCore is recommended. (please note, WindBot does not work with Percy LAN Mode)

Let's start

1. Make a deck

image

When making your deck, remember that the more uses each card has, and the more complicated the deck is, the harder it will be to write your AI.

For this example we will create the ydk file and call it AI_PureWinds and place it in the decks folder of windbot in VS. Then you should add the deck to the project by right clicking the decks folder in VS -> add -> existing item, then selecting your ydk file in its folder and it should then appear in the folder.

2. Create Executor

Executor (not related to the Java one), is used to specify the use of each card for your deck.

Create a new cs code file under Game\AI\Decks and name it PureWindsExecutor.

(When you try to add this, PureWindsExecutor may have already been written, so change it to a name like NewPureWindsExecutor)

Write the following code in it:

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

namespace WindBot.Game.AI.Decks
{
    [Deck("PureWinds", "AI_PureWinds")]

    public class PureWindsExecutor : DefaultExecutor
    {

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

        }
    }
}

It can be seen in WindBot.Game.AI.Decks that the PureWindsExecutor inherits the DefaultExecutor.

The first parameter of the Deck property is the deck name (and also the AI name), and the second part is the deck ydk file name.

3. Take a test

After creating a localhost room in your YGOPro client, use the following parameters to start in VS.

Deck=PureWinds

You will find this by selecting on your VS: Project - Properties - Debug, and you should then have a screen with the picture below:

image

If everything is fine, you should see the AI ​​join the room and tick that it is ready.

  • If AI does not appear, please make sure that Windbot is started after the host is established.

  • If the AI ​​does not tick ready, add the cards.cdb into your bin - debug folder on Windows Explorer and run again.

  • If the AI still does not tick ready, check the ydk file is listed in your decks folder and that the name matches the deck name listed in the Deck property.

After starting the game, you will find that the AI will not play any cards. This is because we have not specified how to use the cards.

WindBot will only use the cards specified in the Executor of the AI script, and the other cards will not be done anything except to attack and use mandatory effects.

So now we have to write how to use each card in the deck.

4. Create a card name class

To facilitate specifying the card name in the code, we create a CardId class and write each card name as a constant.

Create a CardId class in the PureWindsExecutor class (not in the constructor) and define the ID of each card in the deck:

        public class CardId
        {
            public const int SpeedroidTerrortop = 81275020;
            public const int WindwitchIceBell = 43722862;
            public const int PilicaDescendantOfGusto = 71175527;
            public const int SpeedroidTaketomborg = 53932291;
            public const int WindaPriestessOfGusto = 54455435;
            public const int WindwitchGlassBell = 71007216;

            public const int GustoGulldo = 65277087;
            public const int GustoEgul = 91662792;
            public const int WindwitchSnowBell = 70117860;
            public const int SpeedroidRedEyedDice = 16725505;
            public const int Raigeki = 12580477;
            public const int MonsterReborn = 83764719;
            public const int Reasoning = 58577036;
            public const int ElShaddollWinda = 94977269;

            public const int QuillPenOfGulldos = 27980138;
            public const int CosmicCyclone = 8267140;
            public const int EmergencyTeleport = 67723438;

            public const int ForbiddenChalice = 25789292;
            public const int SuperTeamBuddyForceUnite = 8608979;
            public const int KingsConsonance = 24590232;
            public const int GozenMatch = 53334471;
            public const int SolemnStrike = 40605147;
            public const int SolemnWarning = 84749824;

            public const int MistWurm = 27315304;
            public const int CrystalWingSynchroDragon = 50954680;
            public const int ClearWingSynchroDragon = 82044279;
            public const int WindwitchWinterBell = 14577226;

            public const int StardustChargeWarrior = 64880894;
            public const int DaigustoSphreez = 29552709;
            public const int DaigustoGulldos = 84766279;

            public const int HiSpeedroidChanbara = 42110604;
            public const int OldEntityHastorr = 70913714;
            public const int WynnTheWindCharmerVerdant = 30674956;
            public const int GreatFly = 90512490;
            public const int KnightmareIblee = 10158145;
            public const int ChaosMax = 55410871;
            public const int SkillDrain = 82732705;
            public const int SoulDrain = 73599290;
            public const int Rivalry = 90846359;
            public const int OnlyOne = 24207889;
        }

image

You can use any name for the card and even use Japanese or Chinese characters, however this can prove harder to work with and can confuse other people reading the script.

5. Simple Executor: Activate Raigeki and Summon Gusto Gulldo

Except for the deck's Executor, each card should have one or more Executors.

Below we register two Executors in the constructor of PureWindsExecutor:

AddExecutor(ExecutorType.Activate, CardId.Raigeki);

This allows the AI ​​to activate Raigeki when it can be activated.

AddExecutor(ExecutorType.Summon, CardId.GustoGulldo);

This allows the AI ​​to summon Gusto Gulldo when it can normally be summoned.

image

In the main phase for the AI, if there are both Gusto Gulldo and Raigeki in hand, How does it decide the order to play them?

The answer is the order in which you write your Executor's. The AI will always attempt each executor from the top Executor to the bottom. In this case, it will activate Raigeki then summon Gusto Gulldo.

Whenever the AI ​​can activate an effect or summon, it will check the Executors of the script, and determine whether the operations in it are available in order, and if it is available, perform operations.

Then it will start again from the beginning, because YGOPro does not support 2 operations at a time, but resends the list of operations that the client can perform after performing one operation. Eg. If you did not have Raigeki in hand and then it activates an effect to add Raigeki to hand. At the next opportunity it will check again from top to bottom and will activate Raigeki as soon as it is able to do so.

6. Slightly more complicated, only add Windwitch - Snow Bell with Windwitch - Glass Bell, if not already in hand.

Now let 's take a look at the next card. What if we want to Windwitch - Glass Bell to only add Windwitch - Snow Bell without Windwitch - Snow Bell in hand, and add Windwitch - Ice Bell if the AI has Windwitch - Snow Bell in hand?

Create a function under PureWindsExecutor (not in the constructor):

        private bool WindwitchGlassBellEffect()
        {
            if (!Bot.HasInHand(CardId.WindwitchSnowBell))
            {
                AI.SelectCard(CardId.WindwitchSnowBell);
            }
            else if (Bot.HasInHand(CardId.WindwitchSnowBell))
            {
                AI.SelectCard(CardId.WindwitchIceBell);
            }
            return true;
        }

Then register the Windwitch - Glass Bell Executor in the constructor of PureWindsExecutor:

            AddExecutor(ExecutorType.Activate, CardId.WindwitchGlassBell, WindwitchGlassBellEffect);

With this, when Windwitch - Glass Bell is summoned and the effect can be activated, it will add Snow Bell or Ice Bell depending on what is in hand (If you have both in hand, the AI will add Windwitch - Ice Bell).

Bot.HasInMonsterZone, is another example of a condition that can be used, you can also use other HasInGraveyard, HasInSpellZone, HasInMonstersZoneOrInHand and so on.

AI.SelectCard Allows you to select which card is selected for the effect. When the effect activates it will immediately check whether selecting this card is possible and then select it for the effect.

7. Use the default Card Executor, and a trick

The default Executor has been written to include functions for many common use cards. Because the PureWindsExecutor inherits the functions already listed in the DefaultExecutor, some card functions are already written for us..

For example, with the card Cosmic Cyclone.

            AddExecutor(ExecutorType.Activate, CardId.CosmicCyclone, DefaultCosmicCyclone);

DefaultCosmicCyclone has the function to check you have over 1000LP when activated and selects the best card / random card if unknown on your opponent's side of the field. The card should also not activate to destroy your own cards at this point if your opponent has no spells/traps.

Now, say you have two cards that can remove spell/traps on your opponent's side of the field, but one card can destroy multiple spell/traps on your opponent's side and one can only destroy one. If we leave the functions as they are, the function which is listed first on the executor will be used first, which could be a waste using the multiple target card with only one target, or using the single target card then using the multiple targets.

The method we use to fix this, is to register 2 Executors for the multiple target card (in this example) Harpie's Feather Duster. One is to launch when the opponent has more than two spell/trap cards, and the other is to launch when it can. (example below taken from Lightsworn AI)

image

Using the default function from the DefaultExecutor for Harpie's Feather Duster, the system will check to make sure whether there is more than 2 cards on the field before activating. In this example we are using Galaxy Cyclone. Due to the order of the Executors, after attempting the function for DefaultHarpiesFeatherDusterFirst the AI will then attempt to activate Galaxy Cyclone if there is only 1 card. Then, if Galaxy Cyclone is not in the hand, then Harpies Feather Duster will be activated instead.

8. ExecutorType introduction

ExecutorTypeThere are the following:

  • Summon This is used to Normal Summon / Tribute Summon a monster

  • SpSummon This is for cards which can special summon by their own effect (eg: Speedroid Terrortop) or for cards which do not require other card/monster effects to summon. (eg: Synchro, Link, Xyz). This can not be used for Fusion or Ritual Summons.

  • Repos Changes the card between Attack and Defence position.

  • MonsterSet To set a monster in face down defence position.

  • SpellSet Used to set Spells/Traps face down on the field.

  • Activate Used to activate spells/traps from hand/field and any monster effects that can be activated.

  • SummonOrSet If the opponent's monster is stronger the monster will be set in face down defence mode, otherwise the monster will be summoned in face up attack mode.

9. Some "global" variables

Before the function set by each Executor is executed, the variable Card is set to the card currently being judged whether to be launched or summoned.

For example, we want Goblindberg to summon only when you have a 4-star monster other than this card in hand:

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

Similar variables are:

  • ActivateDescription used to determine which effect is activated (refer to the DefaultCastelTheSkyblasterMusketeerEffect)

  • LastChainPlayer The player who activated the effect last time, 0 means himself, 1 means opposite, and -1 means no one (for the summon which don't start a chain)

  • Duel.Player, ChainTargets, LastSummonPlayer, LifePoints, etc.

10. Select multiple cards at the same time

For effects with activation requirements, or effects like Waste Iron Dragon, we need to make multiple selections in one launch. The AI.SelectCard only set one option once.

Then we can use AI.SelectNextCard and AI.SelectThirdCard to achieve this.

Taking the effect of Quill Pen of Gulldos for example, we may wish to select certain cards for the first targets and then the opponents most problematic card for the 2nd target.

        private bool QuillPenOfGulldoseff()
        {
            var gyTargets = Bot.Graveyard.Where(x => x.Attribute == (int)CardAttribute.Wind).Select(x => x.Id).ToArray();
            if (gyTargets.Count() >= 2)
            {
                AI.SelectCard(gyTargets);
                AI.SelectNextCard(Util.GetProblematicEnemyCard());
                return true;
            }
            return false;
        }

This example first makes an array of all cards in the GY which have the Wind Attribute to use as targets under the variable gyTargets.

Of course, the actual code should be more complicated than this, for example you may wish to set a condition for returning Hastorr on your side if equipped to your opponent's monster as a priority. You might also wish to set more specific cards for the first two targets, like selecting Synchro's first.

11. Multiple effects of one card

A card may have multiple effects, such as a Castel, the Skyblaster Musketeer and Minerva, the Exalted Lightsworn. These effects can be written to the same Executor or in multiple Executors.

To determine which effect is activated, you can determine according to the position of the card, and multiple effects in the same position can be determined according to the description of the effect.

image

Take the Executor of the Minerva, the Exalted Lightsworn and the Castel, the Skyblaster Musketeer as an example:

        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;
            }
        }

If Minerva the Exalted's effect is an effect on the field, activate the effect if possible, otherwise, select 3 cards to be destroyed and activate the effect.

        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;
        }

In this example, we tell Castel, the Skyblaster Musketeer's first effect not to activate. The second effect is then activated by targetting the opponent's card that needs to be resolved by using the inbuilt problematic card utility AI.Utils.GetProblematicEnemyCard.

It is worth noting that when ActivateDescription is -1, it means that there are no other effects to choose from.

12. How to enter the battle phase?

The answer is that after all the things that can be done in the main phase are completed, the AI will automatically enter the battle phase.

In other words, all registered Executors are checked several times, and if none of them return true, the AI will enter the battle phase.

It should be noted that this process is not a one-time operation. After launching or calling any card, it will be checked again from the beginning through all the executors, and will do this during every chain or possible interaction.

Specific to the implementation of YGOPro, in the main stage, one is sent MSG_SELECT_IDLECMD to the client from the server, which contains a list of all cards that can be summoned / activated / set. After any operation is processed, the server then resends it MSG_SELECT_IDLECMD.

After WindBot receives MSG_SELECT_IDLECMD , it checks the registered Executors one by one to see if they can be launched or called. The conditions of the Executor can be called to determine whether it needs to be executed. When Windbot re-recieves MSG_SELECT_IDLECMD after the execution it will then repeat this step again.

So, I want to do some operations only in the main phase 2. What should I do?

Duel.Phase == DuelPhase.Main2 Just judge it. Or use AI.Utils.IsTurn1OrMain2().

        public bool IsTurn1OrMain2()
        {
            return Duel.Turn == 1 || Duel.Phase == DuelPhase.Main2;
        }

13. Override OnNewTurn to reset variables that start at the beginning of the round

When activating Windwitch - Ice Bell, we need to check whether Windwitch - Glass Bell has been used. 

At this time we can register a WindwitchGlassBelleff_used variable under PureWindsExecutor, set it to false. When Glass Bell activates the effect, we can then change the variable to true which we will then check before activating Ice Bell's effect. In the case below if the effect has been used, and Snow Bell is not in hand, then the AI doesn't activate Ice Bell's effect.

bool WindwitchGlassBelleff_used = false;

        private bool WindwitchGlassBelleff()
        {
            if (Bot.HasInHandOrHasInMonstersZone(CardId.WindwitchIceBell) && !Bot.HasInHand(CardId.WindwitchSnowBell))
            {
                AI.SelectCard(CardId.WindwitchSnowBell);
                WindwitchGlassBelleff_used = true;
                return true;
            }
            return false;
        }
        private bool WindwitchIceBelleff()
        {
            if (WindwitchGlassBelleff_used && !Bot.HasInHand(CardId.WindwitchSnowBell))
                return false;
            else
                return true;
        }

We also need to reset at the beginning of the round WindwitchGlassBelleff_used.

Executor provides that OnNewTurn, by default, it does nothing at the beginning of the round, and we can override it.

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

14. More Overloads

In addition to OnNewTurn the Executor has many methods that can be overloaded. Some commonly used are as follows:

  • OnSelectHand

    • Pick if the Bot should go first or second when it wins RPS
  • OnSelectCard

    • handles complex selection card situations, for example what should bot discard if it has too many cards in hand.
  • OnSelectPendulumSummon

    • selects monsters to be summoned by pendulum summon
  • OnPreBattleBetween

    • decides whether the attack cannot be carried out

Introduced in detail OnPreBattleBetween.

Before the AI ​​judges whether it can attack, it will call OnPreBattleBetween once with its own attacker and the opponent's defender as parameters. If the return value is false, it means that it cannot attack. But OnPreBattleBetween does not directly compare the attack power, but updates RealPower, compared by other parts of the AI.

All AI that inherit DefaultExecutor has been preloaded with OnPreBattleBetween, and common cards such as Utopia The Lightning and Crystal Wing have their RealPower calculated there.

Let's say we are declaring an attack with a light monster, and we have an Honest in hand, so to calculate RealPower for this battle, we can do something like this:

        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);
        }

If the opponent's defender is not a card such as Utopia the Lightning that prevents Honest from activating the effect, and the attacker is a light attribute and you have Honest, you will update the attacker's RealPower. Then return to the inherited OnPreBattleBetween to continue processing other cases.

15. Some tips for continue writing AI

  • Take a look at AI scripts already written for different decks

  • If you have a question, ask using issues.

  • Many features of WindBot are not perfect, suggestions are welcome.

  • This implementation is far from something like AlphaGo. There is a new AI being worked on that utilizes the Monte Carlo Tree Search algorithm which has been proven successful in Magic the Gathering's AI. For that go here: https://github.com/melvinzhang/yugioh-ai