Lua Tutorial
Lua Tutorial
###Table of Contents###
1. Generic Information
2. Getting Started
3. Breakdown of the ai-template.lua
4. Making our first AI
5. Custom Functions
6. Common problems
7. Card script assistance
8. Adding new decks to the existing AI
9. Fin
In theory, the AI can use any deck you make for it. However, if any
card is not specifically scripted into the AI, it will usually just be
activated whenever possible. Target selection for effects will be
random, so the AI will usually search or target the wrong cards with
its effects.
That is why there are specific decks scripted into the ai file. These
decks are prefixed with the "AI_" prefix to indicate, that the AI can
use them. You may also select the "Random Deck" option, the AI
will then use a random "AI_"- prefixed deck. It is possible to rename
or remove AI decks, so the random deck option does not use those
decks.
Just like the Yugioh cards, the ai files are scripted in Lua, a fairly
common scripting language: http://www.lua.org/about.html
The template holds most of the important functions you can use to
modify the AI's behavior. It also has comments and notes about how
to use these functions. More about that in the detailed breakdown
of the ai-template.lua
For scripting AI files, it is always recommended to have some sort
of syntax check to eliminate some errors before testing the script. If
your IDE supports this already, great.
For my Notepad++, I use this file.
Follow the instructions in the readme to add a syntax check to your
Notepad++.
So, we will open up this file, and have a look at it. Starting at the
top, we have a ton of comments, including version info, changelogs,
some useful tips. After the tips, we have a list of available AI
functions you can call at any time:
AI.GetOppMonsterZones()
AI.GetAIMonsterZones()
AI.GetOppSpellTrapZones()
AI.GetAISpellTrapZones()
AI.GetOppGraveyard()
AI.GetAIGraveyard()
AI.GetOppBanished()
AI.GetAIBanished()
AI.GetOppHand()
AI.GetAIHand()
AI.GetOppExtraDeck()
AI.GetAIExtraDeck()
AI.GetOppMainDeck()
AI.GetAIMainDeck()
All the cards you ever wanted. Pretty self explanatory, these return
a list of all the cards located in the specified location. If there are
no cards in a location, it will return a list filled with nil entrys.
Usage example in the template.
math.randomseed( require("os").time() )
This line is mandatory. I do not know exactly, what it does, it
probably sets up the random seed and syncs it with the system
time. Just include this line into your AI file, it is important.
Next up:
function OnStartOfDuel()
...
end
This function is called by the system automatically at the start of
each duel. In the template, it uses the AI.Chat() function to display
some information.
OnSelectOption
OnSelectEffectYesNo
OnSelectYesNo
OnSelectPosition
OnSelectTribute
OnDeclareMonsterType
OnDeclareAttribute
OnDeclareCard
OnSelectNumber
OnSelectChain
OnSelectSum
OnSelectCard
OnSelectBattleCommand
OnSelectInitCommand
Each of these functions are called by the system, whenever a
situation occurs, where the AI has to take a choice of some sort.
Most of them are pretty self-explanatory, or explained in the
comments of the template. I will go over the important ones later.
card.id
card.original_id
card.cardid
card.description
card.type
card.attack
card.defense
card.base_attack
card.base_defense
card.level
card.base_level
card.rank
card.race
card.attribute
card.position
card.setcode
card.location
card.xyz_material_count
card.xyz_materials
card.owner
card.status
card:is_affected_by(effect_type)
card:get_counter(counter_type)
card.previous_location
card.summon_type
card.lscale
card.rscale
card.equip_count
card:is_affectable_by_chain(index)
card:can_be_targeted_by_chain(index)
card:get_equipped_cards()
card:get_equip_target()
card.text_attack
card.text_defense
Most of them should be pretty obvious again, the others are
explained in the comments. If you have a card object, you have
access to all kinds of information using these.
Great, now we have looked at the template. But what now? How
can we make an actual AI out of this?
First, we will make a copy of the ai-template.lua and rename it. For
this tutorial, we will name it "ai-tutorial.lua". Feel free to clean it up
a bit, you don't really need to keep all the comments, you can
always read them up in the original template. I took the liberty of
preparing a cleaned up version of the file, you can download it here.
Note, that you can already play using this file. As long as it is still in
the "ai" folder and is not named "ai-template.lua", it should be
available. If you try it out and give the AI some random deck, you
will notice, the AI will misplay a LOT, it will just activate every
single card whenever possible, even blowing up its own cards in the
process.
So, how do we handle Breaker now? First, we will have to limit the
AI from using him at will, we want to define the terms of his usage.
So we will need to change the activate function to not activate
everything always, but to make an exception for Breaker. For this,
we will loop all activatable cards, and activate them only, if they are
not Breaker.
So we change this:
for i=1,#cards.activatable_cards do
local c = cards.activatable_cards[i]
if c.id ~= 71413901 then -- Breaker
return COMMAND_ACTIVATE,i
end
end
71413901 is Breaker's ID, the unique number associated with the
card. We will be using these IDs a lot, they are the way to
differentiate the cards in the script. Be careful, it is very easy to
mess up an ID in the script. Now what does this do? This is a
standard for-loop in lua, this will loop through all activatable cards,
and only if the card doesn't have Breaker's ID (~= means "not equal"
in lua), it will be activated. So now, Breaker will never be activated,
ever.
Since that is not exactly what we want, we need to add some sort
of condition, that will allow Breaker to be activated. What would
make sense here? Probably activate Breaker, if the player controls
at least 1 Spell or Trap card.
for i=1,#cards.activatable_cards do
local c = cards.activatable_cards[i]
if c.id == 71413901 then -- Breaker
for j=1,#AI.GetOppSpellTrapZones() do
if AI.GetOppSpellTrapZones()[j] then
return COMMAND_ACTIVATE,i
end
end
end
if c.id ~= 71413901 then
return COMMAND_ACTIVATE,i
end
end
AI.GetOppSpellTrapZones() is one of the functions mentioned in the
template, it returns a list of all of the opponent's Spell&Trap zones.
However, we also need to check, if these zones actually holds any
cards. This means looping through those as well. If a zone is not
occupied, it will be false, otherwise it holds a card. So why aren't
we just using the "#" here? Problem is, it counts the empty S&T
zones as well, so the count for AI.GetOppSpellTrapZones() will
almost always be 8. (5 S/T, 2 Pendulum, 1 field spell).
Now go ahead and test. Breaker shouldn't activate its effect, unless
you control a spell yourself. However, if you do, you will notice the
next problem: Breaker might hold back on his effect activation, but
when he activates, he still destroys the AI's own spell! So how do
we fix that problem?
To select the correct target for Breaker, we will modify the function
like this:
local result = {}
if triggeringID == 71413901 then -- Breaker
for i=1,#cards do
if cards[i].owner == 2 then
return {i}
end
end
end
for i=1,minTargets do
result[i]=i
end
return result
So we are comparing the triggering ID to Breaker, and if it matches,
we loop through all available targets, and return the first one, that
belongs to the opponent. Do note, that the return has to be a list of
indexes, not an index, since some cards require multiple targets. So
you cannot return i here, you have to return {i}. The game will crash
otherwise!
Now try it out. Breaker should only hit the opponent's cards
now. This is how your AI does look like now. Do note, that this is a
very verbose way of doing things. If you add hundreds of cards to
the AI, you'll probably want to make some functions that help you
keeping things short and organized, so you don't have to write out
new loops for every single card etc. I will give a brief overview over
the functions I made to help with this later in the tutorial.
for i=1,#AI.GetOppMonsterZones() do
local c = AI.GetOppMonsterZones()[i]
if c and bit32.band(c.status,STATUS_SUMMONING)>0 then
--c is our summoned monster
end
end
The bit32.band stuff is necessary, because the card may have
multiple statuses, so checking c.status==STATUS_SUMMONING
might be false, because its actual status might be
STATUS_SUMMONING+STATUS_SOMETHING_ELSE. bit32.band
performs a bitwise and operation and can separate the status this
way. Do note, that it returns a number and not true or false. 0 means
it doesn't have the status , >0 has the status.
Now that we have our summoned monster, we can check for its
stats and decide, if TKRO should be activated or not. We will do this
in a separate function for easier handling, lets call it ChainTKRO()
and place it anywhere in the script, outside of the other functions:
function ChainTKRO()
for i=1,#AI.GetOppMonsterZones() do
local c = AI.GetOppMonsterZones()[i]
if c and bit32.band(c.status,STATUS_SUMMONING)>0 then
if c.attack>=1900 then
return true
end
end
end
return false
end
This function is pretty neat, we can call it from anywhere, and it will
return true, if any currently special summoned monster is stronger
than 1900 ATK, and false, if the monster is weaker, or if no monster
with the summon status exists. This allows us to use it in both
Chain and EffectYesNo without having to add the checks for the
summoning to both functions explicitly:
function ChainTKRO()
for i=1,#AI.GetOppMonsterZones() do
local c = AI.GetOppMonsterZones()[i]
if c and bit32.band(c.status,STATUS_SUMMONING)>0 then
if c.attack>=1900 then
return true
end
end
end
return false
end
Alright, that should do it for our very first AI. These are only the very
basics, but we learned a little about how to handle the
OnSelectInitCommand, OnSelectCard, OnSelectEffectYesNo and
OnSelectChain functions. These are by far the most important ones,
I guess about 80% of the entire AI is just handling of these
functions. The basic principle is always the same, a lot of looping,
checking specific conditions, returning the correct index.
Also, do note, that any of the custom functions you use here will not
be available in your custom AIs you built earlier, at least not without
adding them to the AI via a requirement. Adding the following line:
require("ai.ai")
Shorter variables/functions
Stuff you use a LOT should probably be short, so you don't have to
write a whole lot. AIMon() is a shortened version of
AI.GetAIMonsterZones() with all empty zones filtered out already,
which makes it a lot easier to handle for most practical
applications. Similar functions exist in OppMon(), AIGrave(),
AIHand() etc ect. I will list an API of useful custom functions later in
the tutorial.
HasID
Probably the function I use the most: The HasID function is a very
basic looping function. It takes a list of cards and an ID, loops
through the cards and returns true, if the ID is among them. It also
sets a global variable, the GlobalIndex, which can be used to return
the correct index.
So instead of:
for i=1,#cards do
local c = cards()[i]
if c.id == 71564252 and ChainTKRO() then
return 1,i
end
end
I can do this:
Warning: Careful when using HasID again before actually using the
CurrentIndex global, it will override the index. If you want to use it
to check for other cards on the field and still need the global index,
use it with the 3rd parameter as true, which will make it skip the
global index:
BestTargets
TARGET_OTHER
TARGET_DESTROY
TARGET_TOGRAVE
TARGET_BANISH
TARGET_TOHAND
TARGET_TODECK
TARGET_FACEDOWN
TARGET_CONTROL -- as in Snatch Steal
TARGET_BATTLE -- redirects to battle target logic
TARGET_DISCARD
TARGET_PROTECT -- this can be any beneficial effect
It returns a list of indexes based on the card list input, so it is
designed to work in OnSelectCard, obviously. For the example of
Breaker, it would look like this:
For a basic destruction effect with a single target, you only need to
pass the card list, the count defaults to 1, and the target constant
defaults to destroy, making it very easy to use for Breaker. And it
will automatically handle stuff like destruction immune cards or
cards that probably shouldn't be targeted, if known (like Artifacts).
However, it only handles target selection, if there are only bad
targets to choose from, it will still pick one. You'll need an
additional check in OnSelectInit to prevent the activation, if no
favourable targets to destroy exist.
Add
Another attempt at generalizing card target selection, this time for
all kinds of search effects. However this one comes with a catch:
The priority system. To see what I mean, take a look at the
"AIOnDeckSelect" file in the "mods" subfolder of the AI. See that
huge, intimidating bunch of numbers? Yeah, that is, what you will
have to use to make use of the Add function.
So to add a card, you will need to think, how this card is being used.
Do you want it in the hand a lot? Then the first column should
probably be high (I use values from 1 to 10 mostly). Do you need it in
the grave? 5th column for that. Do you only want it in hand, if you
have another card already? High in 1, low in 2, and make a
condition, that returns true, if you have the other card.
This sounds horribly complicated, how does that help us and why do
we need it? Well, many archetypes have tons and tons of search
effects these days. If you want to code all of them specifically, you
would have a lot of repetition, tons of loops checking for specific
cards to find. If you did set up a priority for all cards in your deck,
and you defined proper conditions etc ect, you can use the Add
function for all your search effects now.
PRIO_TOHAND
PRIO_TOFIELD
PRIO_TOGRAVE
PRIO_DISCARD=PRIO_TODECK=PRIO_EXTRA
PRIO_BANISH
The 4th one depends on the deck I am using it in. For Mermails or
Dark World, I handle discards with it (since discarding or sending
from hand to grave makes a huge difference here). Others use it for
shuffling their cards back to the deck, with Daigusto Emeral for
example, or Satellarknight Sirius.
Now, if your priorities and conditions are defined properly, you can
use Add for all kinds of functions, that search or use your own cards
somehow. Summon a chaos monster? Add(cards,PRIO_BANISH) to
banish cards from the grave that like to be banished. Foolish?
Add(cards,PRIO_TOGRAVE) to send the card that works best in the
grave. However, this can become increasingly difficult, especially
for combo decks. Just take a look at the Noble Knight condition
functions at the top of the NobleKnight.lua file. Its a mess :)
You will need to use the AddPriority() function at the start of the
duel to setup your priorities.
CardsMatchingFilter
Another simple looping function, that takes a group and a filter, and
returns, how many cards in that group match the filter. A filter can
be any function, that takes a card and returns a boolean, for
example:
function FilterLevel4(c)
return c.level==4
end
local count = CardsMatchingFilter(cards,FilterLevel4) -- counts all Level 4
monsters in cards
Many of my custom functions support optional filters, and most of
them can pass an additional optional argument for filters with 2
arguments. This allows the use of some generic filters, like this:
local count = CardsMatchingFilter(cards,FilterType,TYPE_MONSTER)
-- counts all monsters in cards
FieldCheck
An easy way to check, how many cards of a specific level the AI
controls, mostly used for XYZ summon checks. FieldCheck(4)
returns the count of all Level 4 monsters the AI currently controls.
This function supports a filter as well,
FieldCheck(4,FilterRace,RACE_WINDBEAST) returns the count of all
Level 4 Winged-Beast type monsters the AI controls.
DestroyCheck
function UseBreaker()
return DestroyCheck(OppST())>0
end
So for something like PWWB, you want to check the targets like
this:
For example, you have a card, that can target all opponent's
monsters, and provides a "cards" list of targets for OnSelectCard.
Now you can call OppMon() in OnSelectCard as well, which, in
theory, provides a list of the exact same monsters. However, the
cards in both lists will be different, you cannot compare them in the
script by just doing something like if cards[i]==OppMon[i]. Even if
those point to the same card, this will return false. However, there
is a card property named card.cardid, which provides a unique ID for
every card used in the duel. So you can use this to compare 2 cards
from different lists, that point to the same card, for our example: if
cards[i].cardid == OppMon[i].cardid
for i=1,#cards do
local c = cards[i]
if c.id == 34086406 and c.description == 545382497 then --Lavalval Chain Dump
Effect
if c.id == 34086406 and c.description == 545382498 then --Lavalval Chain Stack
Effect
Or you use the 4th parameter of HasID:
for i=1,#cards do
local c = cards[i]
if c.id == 34086406 then
print (c.description)
end
end
If the AI controls a single Lavalval Chain and can activate both
effects, this should print: 545382497 545382498
We also need to set up the global variable before activating the effect:
if HasID(cards,34086406,nil,545382497) then --Lavalval Chain Dump Effect
return COMMAND_ACTIVATE,CurrentIndex
end
if HasID(cards,34086406,nil,545382498) then --Lavalval Chain Stack Effect
GlobalCardMode = 1 -- set global variable to 1 to indicate, that the stack
effect is used
return COMMAND_ACTIVATE,CurrentIndex
end
Why do we use both LocCheck and a global variable here? Well, we
can correctly identify the detach target selection. If the target is an
XYZ material, its obviously the detach. But for the other 2
selections, we don't have an easy way to find out, which effect is
used currently just based on the target cards. For both effects, the
targets are in the deck, we cannot check the destination, where the
effect would send them. We could check for spell cards, as the
stack only targets monsters while the dump can also target
spells&traps, but this check might fail for pure monster decks or
late game, if all S/T have been drawn. So to identify this effect
correctly, we need the global variable, at least I think so.
This can be useful for cards, that offer protection against removal
effects, or just to chain cards, that are about to be removed. You
can achieve this by using a function called Duel.GetOperationInfo,
like this:
But luckily for you, I made some custom functions to handle this,
and those are way easier to use. For example, if we want the AI to
chain MST to its own destruction, we can do it like this:
function ChainMST(card)
local targets = DestroyCheck(OppST())
if targets>0 and RemovalCheckCard(card) then
return true
end
return false
end
function OnSelectChain(cards...
if HasID(cards,05318639,ChainMST) then
-- HasID will detect the first parameter after the initial 2 that is a function
and use it as a filter, regardless of parameter order.
return true
end
end
RemovalCheckCard does check, if the card is about to be removed
anywhere in the current chain. It checks for most common ways of
removal, like destroying, banishing, returning to hand or field,
sending to graveyard etc. Optional parameters allow specifications
of categorys, chain links and the type of card, that performs the
removal.
Negating of effects
Very similar to detecting removal effects, we can check the current
effect in the chain, to see, if it should be negated or not, with cards
like Stardust Dragon. Or we can check the entire chain, if something
needs to be negated with cards like Breakthrough Skill etc.
local e = Duel.GetChainInfo(Duel.GetCurrentChain(),CHAININFO_TRIGGERING_EFFECT)
if e then
-- proceed to check e, who owns the card that triggered this effect etc
Of course, we also have some easy to use custom functions for this,
which neatly handle all these things internally and have some
additional perks as well.
For example, most cards, that negate the last chain link specifically
via trigger effect or counter trap or stuff like that, can be handled by
the ChainNegation function. Lets take Wiretap as an example, you
can use it like this in OnSelectChain:
function ChainBTS(card)
local c = ChainCardNegation(card,true) -- the 2nd parameter is for a targeting
check. Use true for targeted negation like BTS, false for non-targeted like Skill
Drain
if c then
GlobalTargetSet(c)
return true
end
end
GlobalTargetSet finds the target on the field and stores it in a global
variable. It is designed to work with GlobalTargetGet, which
recovers the target from a list of cards and is used in OnSelectCard:
function BTSTarget(cards)
return GlobalTargetGet(cards,true) -- 2nd parameter dtermines, if a card should
be returned, or the index of the card in the list. true = index, false = card
end
This will flag the chain link activated by the targeted card as
negated
What are the absolute minimum requirements to add your own deck
to the AI?
DECK_BREAKER = NewDeck("Breaker",71413901,nil)
DECK_BREAKER is the variable holding your deck from now on.
NewDeck has 3 parameters:
1. The name of your deck. Mainly used for displaying the current deck
type during debug mode. Use anything you like, but it should be
informative, like your deck's archetype (Nekroz) or common
abbreviation (HAT).
For the purpose of this example, we will use the name "Breaker".
2. The deck identifier. This can be a card id, or a list of multiple card
ids. You want to use a card, that distinguishes your deck from other
decks. For example, it might be a bad idea to use Fire Formation -
Tenki to identify a Fire Fist deck, because Tenki can be used in
basically anything that runs Beast-Warriors.
For our example, we will use the id of Breaker, 71413901.
3. The startup function. It will be called at the start of the duel, if the
AI detects your deck. Not required, but you probably want to have it.
We will add it later on, use nil for now.
...
require("ai.decks.Constellar")
require("ai.decks.Blackwing")
require("ai.decks.Harpie")
require("ai.decks.Breaker") <--
Thats it, you added your first deck file. Try it out now, start YGOPro
in Debug Mode. For Windows users, this is done by using the 2nd
exe file in your installation folder, called "ygopro_vs_ai_debug.exe".
The game should start, and there should be a DOS window as well,
displaying some text. Users on other operating systems might have
to start YGOPro via console to get those debug messages. Start a
game against your AI decklist. Have a look at the console, it should
display a message at the start of the game (after
rock/paper/scissors): "AI deck is Breaker."
function BreakerStartup(deck)
end
DECK_BREAKER = NewDeck("Breaker",71413901,BreakerStartup)
Note, that the startup function has to be above the other line we
added earlier. The parameter is our deck, the same we stored in
DECK_BREAKER.
function MyDeckStartup(deck)
deck.Init = MyDeckInit
deck.SummonBlacklist = MyDeckSummonBlacklist
end
MyDeckSummonBlacklist = {71413901}
If you set deck.Init to a function, the AI will call it like your standard
OnSelectInitCommand function. Here you can handle all the cards
the same way you would in a seperate AI file.
For a full list of functions and lists available here, refer to the
documented template. The most important ones are probably:
deck.Init -- OnSelectInit
deck.Card -- OnSelectCard
deck.Chain -- OnSelectChain
deck.EffectYesNo -- OnSelectEffectYesNo
From here on, we can script more cards, add more functions as we
need them. The beauty of this is, that you don't need to make your
AI complicated at all. Its perfectly fine to only add an Init function
to handle the summoning of a couple of cards, while still retaining
access to all the features of the standard AI. If thats all the AI
needs, perfect. You can leave out everything else, just make the
file, add the required line + startup function, registed the init
function, and be done with it. Everything you don't specify otherwise
will just be handled by the default AI. You can easily leave out stuff
like staple traps, staple extra deck monsters and the like, if the
default AI already handles them properly. But if the AI misuses a
certain card, just blacklist it and code your own logic instead,
without limiting any other deck, as the blacklist will only be in
effect for your own deck.
The very first step is saving the template under a new name.
Save it as "Breaker.lua" in your "ai/decks" subfolder.
Open the template, and change all references of "MyDeck" to
"Breaker". Most text or code editors have the functionality to
do this automatically with "replace all".
Change the deck name from "My Deck" to "Breaker" as well:
DECK_BREAKER =
NewDeck("Breaker",BreakerIdentifier,BreakerStartup)
BreakerIdentifier = {71413901,71564252}
Target selection:
function BreakerTarget(cards)
return BestTargets(cards,1,TARGET_DESTROY)
end
function BreakerCard(cards,min,max,id,c)
-- add OnSelectCard logic here
if id == 71413901 then
return BreakerTarget(cards)
end
return nil
end
Chaining TKRO:
function TKROFilter(c)
-- a filter checking, if the target should be hit by TKRO
return FilterStatus(c,STATUS_SUMMONING)
and c.attack>=1900
end
function ChainTKRO()
return CardsMatchingFilter(OppMon(),TKROFilter)>0
end
function BreakerChain(cards)
-- add OnSelectChain logic here
if HasID(cards,71564252,ChainTKRO) then
return 1,CurrentIndex
end
return nil
end
function BreakerEffectYesNo(id,card)
-- add OnSelectEffectYesNo logic here
if id == 71564252 and ChainTKRO() then
return true
end
return nil
end
You can check out the end result in this file. Do note, that this can
be expanded upon at will. Currently, the normal summons of the
cards are handled by the default logic, because we did not restrict
them. You can add them to the summon blacklist and add
conditions for their summoning, like only summon Breaker, if the
opponent controls any targets to destroy for his effect. Or only
summon TKRO, if the opponent does not control monsters stronger
than 1900.
###9)Fin###
This concludes the AI Scripting Tutorial, I hope, you can make use
of it. It is a lot of information to absorb, I know. Scripting for the
YGOPro AI is not a trivial matter, there are lots of obstacles on the
way. I look forward to test all your new custom decks, and
eventually integrate them into the AI officially:)