It is currently 18 Apr 2024, 14:37
   
Text Size

Let's talk about suspend...

Moderator: CCGHQ Admins

Let's talk about suspend...

Postby 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:
  • 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? :wink:).
  • 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.
And this is my Keldon Halberdier:
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")) &gt; 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()) &gt; 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>
Let's see each ability that composes the suspend mechanic:
  • 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. :wink:
  • 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.
This is all good and it works if you test it... but there are 2 problems:
  • 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...
User avatar
thefiremind
Programmer
 
Posts: 3515
Joined: 07 Nov 2011, 10:55
Has thanked: 118 times
Been thanked: 721 times

Re: Let's talk about suspend...

Postby RiiakShiNal » 22 Jul 2012, 13:17

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.
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.

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.
RiiakShiNal
Programmer
 
Posts: 2185
Joined: 16 May 2011, 21:37
Has thanked: 75 times
Been thanked: 496 times

Re: Let's talk about suspend...

Postby thefiremind » 22 Jul 2012, 13:27

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.
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.

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...
User avatar
thefiremind
Programmer
 
Posts: 3515
Joined: 07 Nov 2011, 10:55
Has thanked: 118 times
Been thanked: 721 times

Re: Let's talk about suspend...

Postby RiiakShiNal » 22 Jul 2012, 15:05

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.
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: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.
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).
RiiakShiNal
Programmer
 
Posts: 2185
Joined: 16 May 2011, 21:37
Has thanked: 75 times
Been thanked: 496 times

Re: Let's talk about suspend...

Postby 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>
and the AI always activated it when possible, in the few tests I made. A problem would be that, even if the AI decides not to pay, chances are that I would still see the pop-up saying Keldon Halberdier - [playername] is making choices which would spoil the card.
< Former DotP 2012/2013/2014 modder >
Currently busy with life...
User avatar
thefiremind
Programmer
 
Posts: 3515
Joined: 07 Nov 2011, 10:55
Has thanked: 118 times
Been thanked: 721 times

Re: Let's talk about suspend...

Postby 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
User avatar
nabeshin
 
Posts: 207
Joined: 27 Jun 2011, 20:07
Has thanked: 5 times
Been thanked: 31 times

Re: Let's talk about suspend...

Postby sadlyblue » 26 Jul 2012, 09:34

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.
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.
sadlyblue
 
Posts: 175
Joined: 06 Feb 2012, 13:18
Has thanked: 18 times
Been thanked: 16 times

Re: Let's talk about suspend...

Postby thefiremind » 26 Jul 2012, 10:58

sadlyblue wrote:
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.
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.
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.
< Former DotP 2012/2013/2014 modder >
Currently busy with life...
User avatar
thefiremind
Programmer
 
Posts: 3515
Joined: 07 Nov 2011, 10:55
Has thanked: 118 times
Been thanked: 721 times

Re: Let's talk about suspend...

Postby 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>
User avatar
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...

Postby 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 :P).

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! :mrgreen:
< Former DotP 2012/2013/2014 modder >
Currently busy with life...
User avatar
thefiremind
Programmer
 
Posts: 3515
Joined: 07 Nov 2011, 10:55
Has thanked: 118 times
Been thanked: 721 times


Return to Programming Talk

Who is online

Users browsing this forum: No registered users and 13 guests


Who is online

In total there are 13 users online :: 0 registered, 0 hidden and 13 guests (based on users active over the past 10 minutes)
Most users ever online was 4143 on 23 Jan 2024, 08:21

Users browsing this forum: No registered users and 13 guests

Login Form