Monday, November 30, 2009

Shock and Royal Assassin – Part 1

When I first started working on Forge, I could only find two other Magic programs Firemox and Magma. Now there are programs such as Incantus, MagicWars, and Wagic which were inspired by this blog. (On a side note Incantus, MagicWars, and Firemox are like Magic Online and let you play against other people on the Internet with rules enforcement. Wagic and Magma let you play against the computer.)

All of these Magic programs, including my own (Forge), encode cards differently. Today I’m going to show how Forge and Wagic encode two familiar cards: Shock and Royal Assassin. Forge takes the direct route and shoehorns cards into Java while Wagic uses its own scripting language which is very short and concise.

Of the five programs mentioned I believe that Forge’s cards are the longest and Wagic’s are the shortest. Having short cards is a big advantage since it means less code is required for each card and hopefully less debugging. In Forge if you wanted to change the damage code, you would have to update each card that dealt damage, while Wagic would only have to tweak the damage code that is used for all the cards, which is a big improvement.

(This is a contrived example but it is generally true. Cards in Forge always a global static method to deal damage, so the damage code could be updated easily but other aspects of cards such as targeting do not always use a global static method.)

On a side note, usually Forge’s cards are very long but I forgot about the scripting code that people like Rob Cashwalker has been writing. (Since Forge is an open source project, letting other people do some of the “hard stuff” is truly wonderful.) Forge’s Shock is only 5 lines, which is great, and probably wins the title of “shortest Shock code”. So in reality some of Forge’s cards are very long like Royal Assassin and some of the cards are very short like Shock.

Wagic – Royal Assassin
[card]
text={T}: Destroy target tapped creature.
id=129708
auto={T}:destroy target(creature[tapped])
name=Royal Assassin
rarity=R
type=Creature
mana={1}{B}{B}
power=1
subtype=Human Assassin
toughness=1
[/card]

Wagic – Shock
[card]
text=Shock deals 2 damage to target creature or player.
target=creature,player
auto=Damage:2
id=129732
name=Shock
rarity=C
type=Instant
mana={R}
[/card]
Forge - Shock
Shock
R
Instant
no text
spDamageCP:2

Forge – Royal Assassin
if(cardName.equals("Royal Assassin"))
{
final Ability_Tap ability = new Ability_Tap(card)
{

public boolean canPlayAI()
{
CardList human =
CardFactoryUtil.AI_getHumanCreature(card, true);
human = human.filter(new CardListFilter()
{
public boolean addCard(Card c)
{return c.isTapped();}
});

CardListUtil.sortAttack(human);
CardListUtil.sortFlying(human);

if(0 < human.size())
setTargetCard(human.get(0));

return 0 < human.size();
}
public void resolve()
{
Card c = getTargetCard();

if(AllZone.GameAction.isCardInPlay(c) &&
c.isTapped() && CardFactoryUtil.canTarget(card, c) )
{
AllZone.GameAction.destroy(c);
}
}//resolve()
};//SpellAbility

Input target = new Input()
{
public void showMessage()
{
AllZone.Display.showMessage("Select
target tapped creature to destroy");
ButtonUtil.enableOnlyCancel();
}
public void selectButtonCancel() {stop();}
public void selectCard(Card c, PlayerZone zone)
{
if(!CardFactoryUtil.canTarget(card, c)){
AllZone.Display.showMessage(
"Cannot target this card (Shroud? Protection?).");
}
else if(c.isCreature() &&
zone.is(Constant.Zone.Play) && c.isTapped())
{
//tap ability
card.tap();

ability.setTargetCard(c);
AllZone.Stack.add(ability);
stop();
}
}//selectCard()
};//Input

card.addSpellAbility(ability);
ability.setDescription("tap: Destroy target
tapped creature.");
ability.setBeforePayMana(target);
}

3 comments:

nantuko84 said...

wagic code is elegant, but it uses another approach for AI that's why it is hard to compare

and about your code: I can suggest to create generic method or factory for such input, smth like
Input target = TargetInputFactory.getInput("Select
target tapped creature to destroy", new CardListFilter() { public boolean addCard(Card c) {
return c.isCreature() && c.isTapped() }});
as I can see it contains very common code that can be reused for great amount of cards.
e.g. Doom Blade will have:
Input target = TargetInputFactory.getInput("Select
target nonblack creature to destroy", new CardListFilter() { public boolean addCard(Card c) {
return c.isCreature() && !c.getColor().contains(Constant.Color.Black) }});

and one more: why do you tap Assasin in Input (class that is for targeting only)? Royal has Ability_Tap, shouldn't it be tapped somewhere inside engine as a cost?

nantuko84 said...

take into account, it's not a blame, just suggestion ;)

previously I proposed you to use target legality check on resolve, but then realized (thanks to Rob and DennisBergkamp) that it's much harder to you as you need always to keep in mind the AI hard coded in every card

p.s. please check ccgh, I've sent you PM there

Forge said...

Forge does intertwine AI code and card code.

Tap abilities are "hacked" and aren't handled very well. They work according to the rules but the code is ugly and could be refined (as well as the rules logic in general).