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. 组一个卡组
(我不会玩光道,这是我乱组的)
卡组以易用为优先。一张卡的用法越多,写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
如果一切正常,你可以看到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;
}
如果你感觉英文卡名看不懂,也可以用中文,不过这会导致其他人看不懂,自行权衡吧。
5. 最简单的Executor,让AI会发羽毛扫,会通招莱登
除了卡组的Executor,每张卡片都应该有一个或多个Executor。
下面我们在LightswornExecutor的构造函数里注册两个Executor:
AddExecutor(ExecutorType.Activate, CardId.HarpiesFeatherDuster);
这让AI在HarpiesFeatherDuster可以发动时发动它。
AddExecutor(ExecutorType.Summon, CardId.Raiden);
这让AI在Raiden可以通常召唤时召唤它。
在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张以上魔陷时发动,一个是能发就发。
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.IsMonster() && card.Level == 4)
return true;
}
return false;
}
类似的变量还有:
- ActivateDescription
用于判断发动的是哪个效果(参考鸟铳士的
DefaultCastelTheSkyblasterMusketeerEffect
) - LastChainPlayer 上次发动效果的玩家,0表示自己,1表示对面,-1表示没有人(针对不入连锁的召唤的神宣等)
- Duel的Player, ChainTargets, LastSummonPlayer, LifePoints 等属性