Page 1 of 1

Implementing Channel

PostPosted: 02 Jul 2014, 17:27
by ShawnieBoy
I've begun working with the Channel ability word, starting with a simple card first, and have noticed something strange with the AI. Whichever AI I use, it always Channels as soon as it is possible, regardless of whether there's any valid target or not.

The main structure is pretty close to a Cycle activation, but can't work out why the AI's all excited about it...

Code: Select all
name=Shinen of Flight's Wings
image=http://mtgimage.com/card/shinen%20of%20flight%27s%20wings.jpg
value=2.500
rarity=C
type=Creature
subtype=Spirit
cost={4}{U}
pt=3/3
ability=Flying
timing=main
requires_groovy_code
oracle=Flying. Channel — {U}, Discard Shinen of Flight's Wings: Target creature gains flying until end of turn.
Code: Select all
[
    new MagicCardActivation(
        [MagicCondition.HAND_CONDITION],
        new MagicActivationHints(MagicTiming.Pump),
        "Channel"
    ) {
        @Override
        public Iterable<MagicEvent> getCostEvent(final MagicCard source) {
            return [
                new MagicPayManaCostEvent(source, "{U}"),
                new MagicDiscardSelfEvent(source)
            ];
        }
        @Override
        public MagicEvent getEvent(final MagicSource source) {
            return new MagicEvent(
                source,
                MagicTargetChoice.POS_TARGET_CREATURE,
                MagicFlyingTargetPicker.create(),
                this,
                "Target creature\$ gains flying until end of turn."
            );
        }
        @Override
        public void executeEvent(final MagicGame game, final MagicEvent event) {
            event.processTargetPermanent(game,{
                final MagicPermanent creature ->
                game.doAction(new MagicGainAbilityAction(creature,MagicAbility.Flying));
            });
        }
    }
]
Also, as a future note - it will be a bit tricky to get this in the MagicAbility list (I know Channel isn't a keyword ability), as the text it would have to match would be the 'Discard SN' cost, which could possibly pop up on other cards doing different things.

Re: Implementing Channel

PostPosted: 03 Jul 2014, 02:00
by melvin
There are a couple of issue.
1) The getEvent method should return an event that puts the ability on the stack, eg MagicNinjutsuActivation.java

2) getChoice method is missing so the engine is unable to tell that the effect requires a target choice, hence it allowed it to be cast when there is no valid target

I've added MagicChannelActivation that implements getCostEvent, getEvent, getChoice, cards only need extend this by implementing the getCardEvent and executeEvent methods. See https://code.google.com/p/magarena/sour ... 5d5484a401

Re: Implementing Channel

PostPosted: 06 Jul 2014, 13:57
by ShawnieBoy
I've also been looking at Madness:

Code: Select all
name=Ichor Slick
image=http://mtgimage.com/card/ichor%20slick.jpg
value=2.500
rarity=C
type=Sorcery
cost={2}{B}
effect=Target creature gets -3/-3 until end of turn.
ability=Cycling {2}
timing=main
requires_groovy_code
oracle=Target creature gets -3/-3 until end of turn. Cycling {2}. Madness {3}{B}
Code: Select all
[   
    new MagicWhenPutIntoGraveyardTrigger(MagicTrigger.REPLACEMENT) {
        @Override
        public MagicEvent executeTrigger(final MagicGame game, final MagicPermanent permanent, final MagicMoveCardAction act) {
            final MagicCard card = act.card;
            return act.from(MagicLocationType.OwnersHand) && act.to(MagicLocationType.Graveyard) ?
                new MagicEvent(
                    permanent,
                    new MagicMayChoice(
                        new MagicPayManaCostChoice(MagicManaCost.create("{3}{B}"))
                    ),
                    card,
                    this,
                    "PN may\$ cast SN for its madness cost instead of putting it into his or her graveyard."
                ):
            MagicEvent.NONE;
        }
        @Override
        public void executeEvent(final MagicGame game, final MagicEvent event) {
            if (event.isYes()) {
                game.doAction(new MagicPlayCardAction(event.getRefCard()));
            }
        }
    }
]
I'm assuming that the problem is that the Cycling and Discard actions don't reference a game for the executeEvent to refer to.

Essentially Madness is a discard replacement effect with an optional mana payment choice. Discard occurs normally unless the payment is payed, then the card is played instead.

Re: Implementing Channel

PostPosted: 07 Jul 2014, 01:15
by melvin
Your implementation looks reasonable. What is the issue you are facing?

Re: Implementing Channel

PostPosted: 07 Jul 2014, 13:19
by ShawnieBoy
melvin wrote:Your implementation looks reasonable. What is the issue you are facing?
Oh yes, that would have helped...

Having a return of MagicEvent.NONE doesn't quite work as it's a replacement trigger. And as the card is in the process of going to the graveyard you get this:

Code: Select all
Exception from controller.runGame: getGame called for MagicPlayer.NONE
java.lang.RuntimeException: getGame called for MagicPlayer.NONE
   at magic.model.MagicPlayer$1.getGame(MagicPlayer.java:41)
   at magic.model.MagicPermanent.getGame(MagicPermanent.java:342)
   at magic.model.stack.MagicItemOnStack.<init>(MagicItemOnStack.java:37)
   at magic.model.stack.MagicItemOnStack.<init>(MagicItemOnStack.java:41)
   at magic.model.stack.MagicTriggerOnStack.<init>(MagicTriggerOnStack.java:13)
   at magic.model.MagicGame.executeTrigger(MagicGame.java:1267)
   at magic.model.action.MagicMoveCardAction.doAction(MagicMoveCardAction.java:69)
   at magic.model.MagicGame.doAction(MagicGame.java:525)
   at magic.model.action.MagicDiscardCardAction.doAction(MagicDiscardCardAction.java:31)
   at magic.model.MagicGame.doAction(MagicGame.java:525)
   at magic.model.event.MagicDiscardSelfEvent$1.executeEvent(MagicDiscardSelfEvent.java:20)
   at magic.model.event.MagicEvent.executeEvent(MagicEvent.java:631)
   at magic.model.MagicGame.executeEvent(MagicGame.java:722)
   at magic.model.action.MagicExecuteFirstEventAction.doAction(MagicExecuteFirstEventAction.java:18)
   at magic.model.MagicGame.doAction(MagicGame.java:525)
   at magic.model.MagicGame.executeNextEvent(MagicGame.java:751)
   at magic.ai.MCTSAI.getNextChoices(MCTSAI.java:469)
   at magic.ai.MCTSAI.randomPlay(MCTSAI.java:432)
   at magic.ai.MCTSAI.access$000(MCTSAI.java:62)
   at magic.ai.MCTSAI$2.run(MCTSAI.java:199)
   at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
   at java.util.concurrent.FutureTask.run(Unknown Source)
   at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
   at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
   at java.lang.Thread.run(Unknown Source)
Returning the original trigger back with
Code: Select all
: act;
doesn't quite work either, as it's an action not an event.
The MagicGame needs to be returned, but the DiscardCardAction, which is the most likely cause of a card going from your hand to your graveyard, doesn't refer to a MagicGame.

Re: Implementing Channel

PostPosted: 08 Jul 2014, 00:30
by melvin
Returning MagicEvent.NONE is always allowed in triggers, even for replacements.

Rather the issue is probably due to the following:
Code: Select all
new MagicEvent(
  permanent,
  ...
)
permanent could be MagicPermanent.NONE in some cases such as discard from hand to graveyard where there is no permanent involved. Better of using the card as the source of the event.

MagicPermanent.NONE has as its controller MagicPlayer.NONE, hence it failed during the call to getGame(MagicPermanent.java:342) because you're not suppose to call getGame on MagicPlayer.NONE. The message is a bit misleading, will fix it so that it says "getGame called for MagicPermanent.NONE"

Re: Implementing Channel

PostPosted: 08 Jul 2014, 01:15
by ShawnieBoy
I was assuming that the trigger event had to refer to a permanent, even if there isn't one present. All the MagicWhenPutIntoGraveyardTrigger executeTriggers refer to permanents.

Thanks for that :)

Re: Implementing Channel

PostPosted: 08 Jul 2014, 01:47
by ShawnieBoy
I think this could be the basis for a new Ability :)

Code: Select all
[   
    new MagicWhenPutIntoGraveyardTrigger(MagicTrigger.REPLACEMENT) {
        @Override
        public MagicEvent executeTrigger(final MagicGame game, final MagicPermanent permanent, final MagicMoveCardAction act) {
            final MagicCard card = act.card;
            return act.from(MagicLocationType.OwnersHand) && act.to(MagicLocationType.Graveyard) ?
                new MagicEvent(
                    card,
                    new MagicMayChoice(
                        new MagicPayManaCostChoice(MagicManaCost.create("{3}{B}"))
                    ),
                    card,
                    this,
                    "PN may\$ cast SN for its madness cost instead of putting it into his or her graveyard."
                ):
            MagicEvent.NONE;
        }
        @Override
        public void executeEvent(final MagicGame game, final MagicEvent event) {
            if (event.isYes()) {
                final MagicCardOnStack cardOnStack=new MagicCardOnStack(event.getRefCard(),event.getPlayer(),MagicPayedCost.NO_COST);
                game.doAction(new MagicPutItemOnStackAction(cardOnStack));
            }
        }
    }
]
Although I've just noticed that two cards hit the graveyard if Ichor Slick is cycled. It hits the graveyard before paying the mana to cycle it, and the 'May cast' from Madness is on the stack. Still need it to be 'held in limbo' before deciding where it should go.

Re: Implementing Channel

PostPosted: 08 Jul 2014, 03:59
by melvin
It should be exiled if madness cost is paid.

Madness =
"If a player would discard this card, that player discards it, but may exile it instead of putting it into his or her graveyard" and
"When this card is exiled this way, its owner may cast it by paying [cost] rather than paying its mana cost. If that player doesn‘t, he or she puts this card into his or her graveyard."

Re: Implementing Channel

PostPosted: 08 Jul 2014, 04:27
by ShawnieBoy
I was assuming that the replacement to the card movement to the graveyard would prevent the card going to the graveyard at all. Looking at this, it seems as though the replacement isn't replacing anything, just adding to it.

And yes, forgot that Madness was edited in Time Spiral block >.<

Re: Implementing Channel

PostPosted: 08 Jul 2014, 10:36
by melvin
Replacement triggers can modify the destination with act.setLocation, if not the move action takes place unchanged.

Re: Implementing Channel

PostPosted: 08 Jul 2014, 13:36
by ShawnieBoy
Hmm, looking at it, and not seeing a simple way of passing information about an action from an event. I may be better to always assume that the first may choice is always yes (As in, "Do you want to put this card into exile instead of your graveyard?") The results from the second choice still result in the only two possible results: It goes to the graveyard; or it it cast.

Re: Implementing Channel

PostPosted: 08 Jul 2014, 14:29
by ShawnieBoy
Ok, think I've cracked it:
Code: Select all
[   
    new MagicWhenPutIntoGraveyardTrigger(MagicTrigger.REPLACEMENT) {
        @Override
        public MagicEvent executeTrigger(final MagicGame game, final MagicPermanent permanent, final MagicMoveCardAction act) {
            if (act.from(MagicLocationType.OwnersHand) && act.to(MagicLocationType.Graveyard)) {
                game.executeTrigger(MagicTriggerType.WhenOtherPutIntoGraveyard, act); //Activate discard triggers
                act.setToLocation(MagicLocationType.Exile); //Change discard location
                final MagicCard card = act.card;
                return new MagicEvent(
                    card,
                    new MagicMayChoice(
                        new MagicPayManaCostChoice(MagicManaCost.create("{3}{B}"))
                    ),
                    card,
                    this,
                    "PN may\$ cast SN for its madness cost instead of putting it into his or her graveyard."
                );
            } else {
                return MagicEvent.NONE;
            }
        }
        @Override
        public void executeEvent(final MagicGame game, final MagicEvent event) {
            game.doAction(new MagicRemoveCardAction(event.getRefCard(),MagicLocationType.Exile));
            if (event.isYes()) {
                final MagicCardOnStack cardOnStack=new MagicCardOnStack(event.getRefCard(),event.getPlayer(),MagicPayedCost.NO_COST);
                game.doAction(new MagicPutItemOnStackAction(cardOnStack));
            } else {
                game.doAction(new MagicMoveCardAction(event.getRefCard(),MagicLocationType.Exile,MagicLocationType.Graveyard));
            }
        }
    }
]