Board index Programs with AI or Rules Enforcement Magic: The Gathering - Duels of the Planeswalkers Programming Talk
Let's talk about suspend...
Moderator: CCGHQ Admins
Let's talk about suspend...
by thefiremind » 22 Jul 2012, 11:57
I think I made a suspend mechanic that is close enough to the real deal. I'm presenting it so that together we can find eventual improvements or things that should work differently.
First, these are important things you may not know that make my implementation possible:
First, these are important things you may not know that make my implementation possible:
- We can put counters on exiled cards. I think we can put counters on cards no matter where they are (maybe we should thank Aretopolis for that? ).
- Usually you can't make a query that shows exiled cards, but you can do it with the "filterDC" trick that you can see on Wild Pair: you populate a data chest with card pointers, and then you pass it to the query. No matter where those cards are, they will be displayed right in front of the player, as they were in hand or library.
- Code: Select all
<?xml version='1.0'?>
<CARD_V2>
<FILENAME text="KELDON_HALBERDIER_108891" />
<CARDNAME text="KELDON_HALBERDIER" />
<TITLE>
<LOCALISED_TEXT LanguageCode="en-US"><![CDATA[Keldon Halberdier]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[Alabardiere di Keld]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[Keldonischer Hellebardier]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[Hallebardier kelde]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[Alabardero keldon]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[ケルドの矛槍兵]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[Keldon Halberdier]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[Келдонский Алебардщик]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[Alabardeiro Keldoniano]]></LOCALISED_TEXT>
</TITLE>
<MULTIVERSEID value="108891" />
<ARTID value="A108891" />
<ARTIST name="Parente" />
<CASTING_COST cost="{4}{R}" />
<TYPE metaname="Creature" />
<SUB_TYPE metaname="Human" order_de-DE="0" order_es-ES="1" order_fr-FR="0" order_it-IT="1" order_jp-JA="0" order_ko-KR="0" order_pt-BR="0" order_ru-RU="0" />
<SUB_TYPE metaname="Warrior" order_de-DE="1" order_es-ES="0" order_fr-FR="1" order_it-IT="0" order_jp-JA="1" order_ko-KR="1" order_pt-BR="1" order_ru-RU="1" />
<EXPANSION value="TSP" />
<RARITY metaname="C" />
<POWER value="4" />
<TOUGHNESS value="1" />
<STATIC_ABILITY>
<LOCALISED_TEXT LanguageCode="en-US"><![CDATA[First strike]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[Initiative]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[Daña primero.]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[Erstschlag]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[Attacco improvviso]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[先制攻撃]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[선제공격]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[Первый удар]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[Iniciativa]]></LOCALISED_TEXT>
<CONTINUOUS_ACTION>
local characteristics = Object():GetCurrentCharacteristics()
characteristics:Characteristic_Set( CHARACTERISTIC_FIRST_STRIKE, 1 )
</CONTINUOUS_ACTION>
</STATIC_ABILITY>
<ACTIVATED_ABILITY forced_skip="1" active_zone="ZONE_HAND">
<LOCALISED_TEXT LanguageCode="en-US"><![CDATA[Suspend 4—{R}]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[Sospendere 4—{R}]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[Aussetzen 4 — {R}]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[Suspension 4 — {R}]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[Suspender 4—{R}]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[待機 4―{R}]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[Suspend 4—{R}]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[Отсрочка 4—{R}]]></LOCALISED_TEXT>
<LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[Suspender 4—{R}]]></LOCALISED_TEXT>
<COST type="Mana" cost="{R}" />
<AVAILABILITY>
return Object():CanBePlayed( EffectController() ) and
(Object():GetCurrentCharacteristics():Characteristic_Get( CHARACTERISTIC_FLASH ) ~= 0 or
EffectController():SorceryTime() ~= 0)
</AVAILABILITY>
<RESOLUTION_TIME_ACTION>
Object():RemoveFromGame()
</RESOLUTION_TIME_ACTION>
<RESOLUTION_TIME_ACTION>
local suspend = 4
Object():AddCounters( MTG():GetCountersType("TIME"), suspend )
</RESOLUTION_TIME_ACTION>
</ACTIVATED_ABILITY>
<TRIGGERED_ABILITY auto_skip="1" active_zone="ZONE_REMOVED_FROM_GAME">
<TRIGGER value="BEGINNING_OF_STEP" simple_qualifier="controller">
return ( EffectController():MyTurn() ~= 0 ) and ( MTG():GetStep() == STEP_UPKEEP )
</TRIGGER>
<RESOLUTION_TIME_ACTION>
if Object():GetZone() == ZONE_REMOVED_FROM_GAME and Object():CountCounters(MTG():GetCountersType("TIME")) > 0 then
Object():RemoveCounters(MTG():GetCountersType("TIME"), 1)
end
</RESOLUTION_TIME_ACTION>
</TRIGGERED_ABILITY>
<TRIGGERED_ABILITY auto_skip="1" active_zone="ZONE_REMOVED_FROM_GAME">
<TRIGGER value="COUNTERS_CHANGED" simple_qualifier="self">
return CounterTypeIndex() == MTG():GetCountersType("TIME")
</TRIGGER>
<RESOLUTION_TIME_ACTION>
if Object():GetZone() == ZONE_REMOVED_FROM_GAME and Object():CountCounters(CounterTypeIndex()) > 0 then
local filterDC = EffectDC():Make_Chest(0)
filterDC:Set_CardPtr(0, Object())
for i=0,MTG():GetNumberOfPlayers()-1 do
local nthPlayer = MTG():GetNthPlayer(i)
if nthPlayer ~= nil and nthPlayer:IsAI() == 0 then
nthPlayer:ChooseTargetFromDCWithFlags( NO_VALIDATION, "CARD_QUERY_LOOK_AT_SUSPENDED_CARD", filterDC,
EffectDC():Make_Chest(1), QUERY_FLAG_CAN_BE_FINISHED_EARLY )
end
end
end
</RESOLUTION_TIME_ACTION>
</TRIGGERED_ABILITY>
<TRIGGERED_ABILITY auto_skip="1" priority="-1" active_zone="ZONE_REMOVED_FROM_GAME">
<TRIGGER value="COUNTERS_CHANGED" simple_qualifier="self">
return CounterTypeIndex() == MTG():GetCountersType("TIME") and Object():CountCounters(CounterTypeIndex()) == 0
</TRIGGER>
<RESOLUTION_TIME_ACTION>
if Object():GetZone() == ZONE_REMOVED_FROM_GAME and Object():CanBePlayed( Object():GetOwner() ) then
Object():PlayFreeFromAnywhere( Object():GetOwner() )
if Object():GetCardType():Test( CARD_TYPE_CREATURE ) ~= 0 then
EffectDC():Set_Int(2, 1)
end
end
</RESOLUTION_TIME_ACTION>
<RESOLUTION_TIME_ACTION>
if EffectDC():Get_Int(2) == 1 then
local delayDC = EffectDC():Make_Chest(1)
delayDC:Set_CardPtr(0, Object())
delayDC:Protect_CardPtr(0)
MTG():CreateDelayedTrigger(1, delayDC)
end
</RESOLUTION_TIME_ACTION>
</TRIGGERED_ABILITY>
<TRIGGERED_ABILITY resource_id="1" internal="1" filter_zone="ZONE_IN_PLAY">
<CLEANUP fire_once="1" />
<TRIGGER value="ZONECHANGE_END" to_zone="ZONE_IN_PLAY" from_zone="ZONE_STACK">
return EffectDC():Get_CardPtr(0) ~= nil and
TriggerObject() == EffectDC():Get_CardPtr(0) and
TriggerObject():GetErstwhileErstwhileZone() == ZONE_REMOVED_FROM_GAME
</TRIGGER>
<CONTINUOUS_ACTION layer="6">
local object = EffectDC():Get_CardPtr(0)
if object ~= nil then
local characteristics = object:GetCurrentCharacteristics()
characteristics:Characteristic_Set( CHARACTERISTIC_HASTE, 1 )
end
</CONTINUOUS_ACTION>
<DURATION>
local object = EffectDC():Get_CardPtr(0)
return object == nil or object:GetZone() ~= ZONE_IN_PLAY or object:GetController() ~= EffectController()
</DURATION>
</TRIGGERED_ABILITY>
<HELP title="MORE_INFO_BADGE_TITLE_0" body="MORE_INFO_BADGE_BODY_0" zone="ZONE_ANY" />
<SFX text="COMBAT_CHOP_LARGE_ATTACK" power_boundary_min="4" power_boundary_max="-1" />
<SFX text="COMBAT_CHOP_SMALL_ATTACK" power_boundary_min="1" power_boundary_max="3" />
</CARD_V2>
- The first ability is activated and it works from the hand. Rules say that you must be able to play the card normally if you want to suspend it, so I'm checking that the card can be played and that either it has flash or it's sorcery time (on instants we would make only the first check, of course). When I activate the ability, I remove the card from game and place the right number of counters. It's on forced_skip because rules say that you can't answer to suspend abilities. But don't worry, players will have a feedback on each suspended card, just wait and see.
- The second ability is triggered and it removes a time counter at the beginning of the owner's upkeep. I'm not checking if I removed the last counter, I'll do that in another ability because the two events are unrelated: if I remove the last counter by other means, it must work anyway.
- The third ability is triggered and it serves for the feedback I was talking about before: when the number of time counters on the card changes without going to 0, every non-AI player gets a query that does nothing if you choose the card, but it shows the card so that everyone knows the current number of counters.
- The fourth ability triggers when the last counter is removed: it plays the card, and then it sets the delayed trigger that gives haste if it's a creature. Why did I use 2 separate actions? Because it seems that Protect_CardPtr protects the pointer for 1 zone change only, so I had to wait for the card to be on the stack before protecting the card pointer.
- The fifth ability gives haste to the card when it enters the battlefield after being suspended.
- You have to turn on the option for keeping priority if you want to be able to play suspend abilities in sorcery time when you don't have enough mana to play anything else, otherwise the game will just move on.
- The AI doesn't understand how to use suspend, and I think it's not just because I didn't write the AI_AVAILABILITY. Either we add a triggered ability that forces the AI to suspend the cards when it's possible, or... I don't know.
< Former DotP 2012/2013/2014 modder >
Currently busy with life...
Currently busy with life...
-
thefiremind - Programmer
- Posts: 3515
- Joined: 07 Nov 2011, 10:55
- Has thanked: 118 times
- Been thanked: 721 times
Re: Let's talk about suspend...
by RiiakShiNal » 22 Jul 2012, 13:17
I think the AI doesn't use suspend because it can't properly figure it out because it seems it can only look as far ahead as main 2 of the next turn, so it might be able to figure out suspend 1 (and even then only on higher difficulties), but nothing higher. Because it can't look too far ahead it probably thinks that the ability just removes the card from the game with some counters and nothing more.thefiremind wrote:The AI doesn't understand how to use suspend, and I think it's not just because I didn't write the AI_AVAILABILITY. Either we add a triggered ability that forces the AI to suspend the cards when it's possible, or... I don't know.
If you give the counters a score and give the card a high enough score in exile with counters then the AI might consider using the suspend ability even though it can't see much farther ahead, but it's also possible that won't work either.
Just getting started: Xander9009's DotP 2014 Community Wad
Need a deck builder: DotP 2014 Deck Builder
Problems Modding: DotP 2014 Frequent Modding Mistakes
Need a deck builder: DotP 2014 Deck Builder
Problems Modding: DotP 2014 Frequent Modding Mistakes
- RiiakShiNal
- Programmer
- Posts: 2185
- Joined: 16 May 2011, 21:37
- Has thanked: 75 times
- Been thanked: 497 times
Re: Let's talk about suspend...
by thefiremind » 22 Jul 2012, 13:27
That's definitely worth trying. I'll test it. Even if it's a bit counter-intuitive because when we suspend a card, the less counters it has, the more we are happy. So the counters should actually decrease the score.RiiakShiNal wrote:If you give the counters a score and give the card a high enough score in exile with counters then the AI might consider using the suspend ability even though it can't see much farther ahead, but it's also possible that won't work either.
It's also possible that the AI always behaves as a player that chose not to keep priority, so it skips the main phases when there's nothing to play and can't even try to consider suspending something.
< Former DotP 2012/2013/2014 modder >
Currently busy with life...
Currently busy with life...
-
thefiremind - Programmer
- Posts: 3515
- Joined: 07 Nov 2011, 10:55
- Has thanked: 118 times
- Been thanked: 721 times
Re: Let's talk about suspend...
by RiiakShiNal » 22 Jul 2012, 15:05
I agree that it is counter-intuitive, but since the AI doesn't look ahead that far it may be necessary to get it to use the ability at all.thefiremind wrote:That's definitely worth trying. I'll test it. Even if it's a bit counter-intuitive because when we suspend a card, the less counters it has, the more we are happy. So the counters should actually decrease the score.
That is also possible in which case we may never get the AI to properly suspend something. Though in some respects the player should never have the option to cast it because the suspend mechanic requires that the card be cast when the last time counter is removed so it should be cast regardless of whether hold priority is selected or not (though I do understand engine limitations can limit our implementations).thefiremind wrote:It's also possible that the AI always behaves as a player that chose not to keep priority, so it skips the main phases when there's nothing to play and can't even try to consider suspending something.
Just getting started: Xander9009's DotP 2014 Community Wad
Need a deck builder: DotP 2014 Deck Builder
Problems Modding: DotP 2014 Frequent Modding Mistakes
Need a deck builder: DotP 2014 Deck Builder
Problems Modding: DotP 2014 Frequent Modding Mistakes
- RiiakShiNal
- Programmer
- Posts: 2185
- Joined: 16 May 2011, 21:37
- Has thanked: 75 times
- Been thanked: 497 times
Re: Let's talk about suspend...
by thefiremind » 22 Jul 2012, 15:49
I added this:
- Code: Select all
<TRIGGERED_ABILITY forced_skip="1" active_zone="ZONE_HAND">
<TRIGGER value="END_OF_STEP" simple_qualifier="controller">
return EffectController():MyTurn() ~= 0 and EffectController():IsAI() ~= 0 and
MTG():GetStep() == STEP_MAIN_2 and Object():CanBePlayed( EffectController() ) and
EffectController():CanAfford("{R}") == 1
</TRIGGER>
<COST type="Mana" cost="{R}" qualifier="conditional" />
<RESOLUTION_TIME_ACTION conditional="if">
Object():RemoveFromGame()
</RESOLUTION_TIME_ACTION>
<RESOLUTION_TIME_ACTION conditional="if">
local suspend = 4
Object():AddCounters( MTG():GetCountersType("TIME"), suspend )
</RESOLUTION_TIME_ACTION>
</TRIGGERED_ABILITY>
< Former DotP 2012/2013/2014 modder >
Currently busy with life...
Currently busy with life...
-
thefiremind - Programmer
- Posts: 3515
- Joined: 07 Nov 2011, 10:55
- Has thanked: 118 times
- Been thanked: 721 times
Re: Let's talk about suspend...
by nabeshin » 25 Jul 2012, 21:49
I will tell about that as far sees AI - often screw me with my Phthisis without leaving me a choice.
ps - i checked this - my variant suxx too... ai don't use jhoira's time bug earlier to receive epochrasit - 4/4 , usually he counts the protection perfectly
ps - i checked this - my variant suxx too... ai don't use jhoira's time bug earlier to receive epochrasit - 4/4 , usually he counts the protection perfectly
Re: Let's talk about suspend...
by sadlyblue » 26 Jul 2012, 09:34
But i think AI works well with Igneous Pouncer and Jhessian Zombies. I know the results are imediate (gets the land) and can use it at any time.thefiremind wrote:It's also possible that the AI always behaves as a player that chose not to keep priority, so it skips the main phases when there's nothing to play and can't even try to consider suspending something.
Re: Let's talk about suspend...
by thefiremind » 26 Jul 2012, 10:58
Cycling can be used as instant, so the AI can use it outside of the main phases and it doesn't matter if the main phases are skipped.sadlyblue wrote:But i think AI works well with Igneous Pouncer and Jhessian Zombies. I know the results are imediate (gets the land) and can use it at any time.thefiremind wrote:It's also possible that the AI always behaves as a player that chose not to keep priority, so it skips the main phases when there's nothing to play and can't even try to consider suspending something.
< Former DotP 2012/2013/2014 modder >
Currently busy with life...
Currently busy with life...
-
thefiremind - Programmer
- Posts: 3515
- Joined: 07 Nov 2011, 10:55
- Has thanked: 118 times
- Been thanked: 721 times
Re: Let's talk about suspend...
by kevlahnota » 27 Jul 2012, 08:03
can you add something like this if the AI will suspend the card:
- Code: Select all
<AI_CUSTOM_SCORE zone="ZONE_ANY">
if Object():GetZone() == ZONE_REMOVED_FROM_GAME then
Object():AddScore(1200)
end
</AI_CUSTOM_SCORE>
-
kevlahnota - Programmer
- Posts: 825
- Joined: 19 Jul 2010, 17:45
- Location: Philippines
- Has thanked: 14 times
- Been thanked: 264 times
Re: Let's talk about suspend...
by thefiremind » 28 Feb 2013, 21:59
I know, today I'm jumping from a mechanic to another... but NEMESiS made me remember that suspend is one of my favorite mechanics so I made another attempt on this, and I discovered something useful.
First of all, it's not true that the AI is like a player who never keeps priority: if it has a spell/ability with a "main_1" and/or "main_2" AI_AVAILABILITY, and it decides to cast/activate that, it will stop on the main phase to do that. That means that giving AI_AVAILABILITY to the suspend ability is mandatory.
But this is not enough. I discovered why the AI doesn't suspend cards: since it cannot look ahead enough to see when the suspended card will be cast, it just ponders if keeping the card in hand is more valuable than dumping it into exile. The solution is easy: give the card a negative AI_BASE_SCORE on ZONE_HAND. How much? I don't know exactly, but I know that it varies according to the card. Testing various times with increments of -150 (it seems that a generic ability is worth 150 so I'm using it as "unit") I had to give -600 to Keldon Halberdier to convince the AI to suspend it, but it wasn't enough for Errant Ephemeron, which has been suspended only when I reached -750. Raising the AI_BASE_SCORE on ZONE_REMOVED_FROM_GAME seems to have no impact at all.
So, suspend is possible after all, but I need a lot of patience to test cards one by one and see what's the best AI_BASE_SCORE to let the AI suspend the card (and I don't want to just give -5000 to all of them ).
EDIT: Just as an additional information: I needed -1050 for Deep-Sea Kraken.
EDIT 2: I think I'll be able to make a suspend deck soon. It will also feature Jhoira of the Ghitu, and thanks to an invisible token that will manage the suspend mechanic, I might even be able to make cards that remove/add time counters on suspended cards (but I can't be sure about this yet). Stay tuned!
First of all, it's not true that the AI is like a player who never keeps priority: if it has a spell/ability with a "main_1" and/or "main_2" AI_AVAILABILITY, and it decides to cast/activate that, it will stop on the main phase to do that. That means that giving AI_AVAILABILITY to the suspend ability is mandatory.
But this is not enough. I discovered why the AI doesn't suspend cards: since it cannot look ahead enough to see when the suspended card will be cast, it just ponders if keeping the card in hand is more valuable than dumping it into exile. The solution is easy: give the card a negative AI_BASE_SCORE on ZONE_HAND. How much? I don't know exactly, but I know that it varies according to the card. Testing various times with increments of -150 (it seems that a generic ability is worth 150 so I'm using it as "unit") I had to give -600 to Keldon Halberdier to convince the AI to suspend it, but it wasn't enough for Errant Ephemeron, which has been suspended only when I reached -750. Raising the AI_BASE_SCORE on ZONE_REMOVED_FROM_GAME seems to have no impact at all.
So, suspend is possible after all, but I need a lot of patience to test cards one by one and see what's the best AI_BASE_SCORE to let the AI suspend the card (and I don't want to just give -5000 to all of them ).
EDIT: Just as an additional information: I needed -1050 for Deep-Sea Kraken.
EDIT 2: I think I'll be able to make a suspend deck soon. It will also feature Jhoira of the Ghitu, and thanks to an invisible token that will manage the suspend mechanic, I might even be able to make cards that remove/add time counters on suspended cards (but I can't be sure about this yet). Stay tuned!
< Former DotP 2012/2013/2014 modder >
Currently busy with life...
Currently busy with life...
-
thefiremind - Programmer
- Posts: 3515
- Joined: 07 Nov 2011, 10:55
- Has thanked: 118 times
- Been thanked: 721 times
10 posts
• Page 1 of 1
Who is online
Users browsing this forum: No registered users and 25 guests