Filters, Card Objects, and Group Objects
Most card scripts spend a lot of time answering one question: "which cards count?" Filters, Card objects, and Group objects are the normal tools for that job.
Filter Shape
A filter is a Lua function that receives a candidate Card first and returns true or false.
function s.filter(c)
return c:IsFaceup() and c:IsSetCard(0x123)
endKeep a filter focused. It should describe one rule clearly: face-up monsters, cards that can be sent to the GY, cards in a Set, monsters that can be Special Summoned, and so on.
Filters should be safe to call more than once. Do not select cards, move cards, show prompts, set labels, or change the Duel from inside an ordinary card filter. The engine may call the filter repeatedly while it checks legality, builds a Group, displays a prompt, or compares candidates.
Mirror Force (c44095762.lua) keeps the filter small and pure:
function c44095762.filter(c)
return c:IsAttackPos()
endThe target and operation callbacks decide when to build Groups and when to destroy cards. The filter only answers whether a candidate card is in Attack Position.
Predefined Functions As Filters
Many Card.* functions can be passed directly when they already answer the question you need.
Duel.IsExistingMatchingCard(Card.IsFaceup,tp,LOCATION_MZONE,0,1,nil)Extra arguments work the same way as custom filters.
local p=Duel.GetTurnPlayer()
Duel.IsExistingMatchingCard(Card.IsControler,tp,LOCATION_ONFIELD,LOCATION_ONFIELD,1,nil,p)Each candidate card becomes the first argument to Card.IsControler, and p is passed after it.
Passing Extra Arguments
Many Duel and Group functions pass extra arguments to the filter after the candidate card.
function s.spfilter(c,e,tp)
return c:IsLevelBelow(4)
and c:IsCanBeSpecialSummoned(e,0,tp,false,false)
end
local g=Duel.GetMatchingGroup(s.spfilter,tp,LOCATION_GRAVE,0,nil,e,tp)Here each filter call receives c,e,tp. The nil before e,tp is the excluded card or group.
Signature Map
Most filter APIs have the same mental shape:
API(filter, player, location1, location2, min_or_count, excluded, extra...)The API chooses each candidate card and calls:
filter(candidate, extra...)That means the first argument in your filter is always the candidate card, not the activating effect or the player. Pass e, tp, archetype codes, levels, or other comparison data after the excluded argument.
function s.thfilter(c,setcode)
return c:IsSetCard(setcode) and c:IsAbleToHand()
end
Duel.IsExistingMatchingCard(s.thfilter,tp,LOCATION_GRAVE,0,1,nil,0x123)This is also why direct Card.* functions work as filters when their argument order already matches the question.
The Excluded Card Or Group
Matching APIs commonly include an ex argument. It can be nil, a Card, or in some APIs a Group. Matching candidates equal to the excluded card/group are skipped.
local g=Duel.GetMatchingGroup(s.filter,tp,LOCATION_MZONE,0,e:GetHandler())Use this when the handler should not select itself, or when a later selection must exclude cards already chosen.
Finding vs Selecting
Checking whether cards exist and selecting cards are separate actions.
| Need | Common API |
|---|---|
| Check non-targeting cards. | Duel.IsExistingMatchingCard |
| Select non-targeting cards. | Duel.SelectMatchingCard |
| Check official effect targets. | Duel.IsExistingTarget |
| Select official effect targets. | Duel.SelectTarget |
| Collect every matching card into a Group. | Duel.GetMatchingGroup |
The difference matters. If the card text targets, use target functions. If it does not target, selection often happens during resolution with matching-card functions.
For deeper selection workflows, including subgroup choices, SelectUnselect, and keeping Groups alive for delayed effects, see Selection And Group Handling.
Which Selector Should I Reach For?
Use this quick rule before writing the callback:
| Text or script need | Check with | Select with |
|---|---|---|
| The card text says "target". | Duel.IsExistingTarget | Duel.SelectTarget |
| The effect chooses cards during resolution. | Duel.IsExistingMatchingCard | Duel.SelectMatchingCard |
| You already collected a candidate Group. | Group.IsExists or #g | Group.Select or Group.FilterSelect |
| You need all matching cards, not player choice. | Duel.GetMatchingGroupCount | Duel.GetMatchingGroup |
Do not use Duel.SelectTarget just because a player chooses a card. In script terms, targeting is a specific activation-time promise stored on the Chain Link.
Targeting Flow
Targeting callbacks usually include both a chkc branch and a chk==0 branch.
function s.target(e,tp,eg,ep,ev,re,r,rp,chk,chkc)
if chkc then
return chkc:IsControler(tp) and chkc:IsLocation(LOCATION_MZONE)
and s.filter(chkc)
end
if chk==0 then
return Duel.IsExistingTarget(s.filter,tp,LOCATION_MZONE,0,1,nil)
end
Duel.Hint(HINT_SELECTMSG,tp,HINTMSG_TARGET)
Duel.SelectTarget(tp,s.filter,tp,LOCATION_MZONE,0,1,1,nil)
endchkc tells the client whether a manually chosen card is legal. The chk==0 branch only proves that at least one legal target exists. The real activation branch selects the target.
Groups Are Containers
A Group is a collection of cards. It is not a single card.
local g=Duel.GetMatchingGroup(s.filter,tp,LOCATION_MZONE,0,nil)
if #g>0 then
Duel.Hint(HINT_SELECTMSG,tp,HINTMSG_TOGRAVE)
local sg=g:Select(tp,1,1,nil)
Duel.SendtoGrave(sg,REASON_EFFECT)
endUse #g or g:GetCount() when you only need the number of cards.
Do not call Card methods on a Group.
local g=Duel.SelectMatchingCard(tp,s.filter,tp,LOCATION_MZONE,0,1,1,nil)
-- Wrong: g:IsAttribute(ATTRIBUTE_DARK)
local tc=g:GetFirst()
if tc and tc:IsAttribute(ATTRIBUTE_DARK) then
-- work with the card
endIf a script reports an error like "attempt to call a nil value" on a card method, check whether the variable is actually a Group.
Getting Cards From A Group
When the group is known to contain one selected card, GetFirst is common.
local tc=g:GetFirst()
if tc then
Duel.SendtoHand(tc,nil,REASON_EFFECT)
endWhen you need to process every card, use aux.Next(g) in modern scripts.
for tc in aux.Next(g) do
tc:RegisterFlagEffect(id,RESET_EVENT+RESETS_STANDARD,0,1)
endOlder local scripts often use GetFirst and GetNext.
local tc=g:GetFirst()
while tc do
-- work with tc
tc=g:GetNext()
endBoth patterns read cards from a Group; they do not create new selections by themselves.
For a Group that contains exactly two cards, GetFirst and GetNext can split them into two variables.
local c1=g:GetFirst()
local c2=g:GetNext()
if c1 and c1:IsControler(1-tp) then
c1,c2=c2,c1
endThat swap pattern is useful when the two cards need stable roles, such as "my card" and "opponent's card."
Local scripts also use two-card Groups for stricter paired checks. In c99742859.lua, the script first tests whether a candidate group has two cards with different race, attribute, and Level values, then splits the selected group:
function s.spcheck(g)
return #g==2
and g:GetClassCount(Card.GetRace)==2
and g:GetClassCount(Card.GetAttribute)==2
and g:GetClassCount(Card.GetLevel)==2
end
local sg=g:SelectSubGroup(tp,s.spcheck,false,2,2)
if sg then
local tc1=sg:GetFirst()
local tc2=sg:GetNext()
Duel.SpecialSummonStep(tc1,0,tp,tp,false,false,POS_FACEUP)
Duel.SpecialSummonStep(tc2,0,tp,tp,false,false,POS_FACEDOWN_DEFENSE)
Duel.SpecialSummonComplete()
endThat is a good pattern when "choose two cards that satisfy a relationship" cannot be expressed by a one-card filter alone.
Filtering A Group
If you already have a Group, filter it instead of scanning the field again.
local tg=Duel.GetChainInfo(0,CHAININFO_TARGET_CARDS)
local rg=tg:Filter(Card.IsRelateToEffect,nil,e)This is a common resolution pattern: keep only targets still related to the resolving effect, then apply the operation to that smaller Group.
Group.FilterSelect combines filtering and player selection.
local sg=g:FilterSelect(tp,Card.IsAbleToGrave,1,1,nil)Use it when the candidate pool is already in a Group.
Group.Filter returns a new Group. Group.FilterSelect asks the player to choose from the filtered result.
local faceup=g:Filter(Card.IsFaceup,nil)
local sendable=faceup:Filter(Card.IsAbleToGrave,nil)local sg=g:FilterSelect(tp,Card.IsAbleToGrave,1,1,nil)If the effect moves every card that still qualifies, filter. If the player chooses some of them, filter-select or select from the filtered Group.
Other Ways To Create Groups
Duel.GetMatchingGroup uses a filter while collecting the cards.
local g=Duel.GetMatchingGroup(Card.IsFaceup,tp,LOCATION_MZONE,0,nil)Duel.GetFieldGroup collects all cards in locations without a filter.
local g=Duel.GetFieldGroup(tp,LOCATION_ONFIELD,0)
local faceup=g:Filter(Card.IsFaceup,nil)Use GetFieldGroup when the starting pool is "everything in these locations." Use GetMatchingGroup when the filter is part of the collection itself.
Checking A Group
Group.IsExists asks whether at least a number of cards in the Group match a filter.
if g:IsExists(Card.IsRace,1,nil,RACE_DRAGON) then
-- at least one Dragon is in the Group
endGroup.IsContains asks whether a specific card is in the Group.
if c:GetEquipGroup():IsContains(e:GetHandler()) then
-- this card is among the equipped cards
endUse IsExists for a rule and IsContains for membership of a known card.
Checking Or Selecting Subgroups
Some effects need a valid set, not just valid individual cards: two monsters with different names, monsters whose Levels add up, or multiple cards with different races. Use subgroup helpers for those.
function s.diffcheck(g)
return #g==2 and g:GetClassCount(Card.GetCode)==2
end
if g:CheckSubGroup(s.diffcheck,2,2) then
local sg=g:SelectSubGroup(tp,s.diffcheck,false,2,2)
endCheckSubGroup belongs in activation legality. SelectSubGroup belongs where the player actually chooses. If the chosen Group must survive until a delayed operation, call KeepAlive and later DeleteGroup; see Creation, building, and registration of an effect object.
Counting
Use the count helper that matches the data you have.
| Data you have | Count pattern |
|---|---|
| A Group | #g or g:GetCount() |
| A filtered Group | g:Filter(...):GetCount() |
| Cards in field locations | Duel.GetMatchingGroupCount(...) |
| Cards in a simple location | Duel.GetFieldGroupCount(player,loc1,loc2) |
Other count helpers, such as Group.FilterCount and class-count helpers, are useful when the Group already exists and the count depends on a filter or on distinct values. Prefer the most direct API for the question you are asking.
Do not build a Group just to count cards if a count API already answers the question.
A Practical Selection Pattern
This complete flow checks for a card in the GY, selects it, then sends it to the hand.
function s.thfilter(c)
return c:IsSetCard(0x123) and c:IsAbleToHand()
end
function s.thtg(e,tp,eg,ep,ev,re,r,rp,chk)
if chk==0 then
return Duel.IsExistingMatchingCard(s.thfilter,tp,LOCATION_GRAVE,0,1,nil)
end
Duel.SetOperationInfo(0,CATEGORY_TOHAND,nil,1,tp,LOCATION_GRAVE)
end
function s.thop(e,tp,eg,ep,ev,re,r,rp)
Duel.Hint(HINT_SELECTMSG,tp,HINTMSG_ATOHAND)
local g=Duel.SelectMatchingCard(tp,s.thfilter,tp,LOCATION_GRAVE,0,1,1,nil)
if #g>0 then
Duel.SendtoHand(g,nil,REASON_EFFECT)
Duel.ConfirmCards(1-tp,g)
end
endBecause this example does not target, the selected card is chosen during resolution.
Useful API pages:
Duel.GetMatchingGroupDuel.IsExistingMatchingCardDuel.SelectMatchingCardDuel.IsExistingTargetDuel.SelectTargetGroup.FilterGroup.FilterSelectGroup.FilterCountGroup.GetClassCountGroup.CheckSubGroupGroup.SelectSubGroupGroup.SelectGroup.IsExistsGroup.IsContainsGroup.GetFirstGroup.GetNext- Selection And Group Handling
- Methods for Affecting Cards