Programming Magic The Gathering - Part 2: Subject to Change
2020-10-30
Progress is good - I've got static abilities working! Sort of.
Static Abilities
I'm now at the point where I can key in cards to test with! I've made a test program that makes a game with two opponents with libraries full of vanilla creatures with no abilities.
var ogre = new MTGLib.MTGObject.BaseCardAttributes()
{
name = "Onakke Ogre",
manaCost = new ManaCost(
2, ManaSymbol.Red
),
power = 4,
toughness = 2,
cardTypes = new HashSet<MTGLib.MTGObject.CardType> { MTGLib.MTGObject.CardType.Creature },
subTypes = new HashSet<MTGLib.MTGObject.SubType> { MTGLib.MTGObject.SubType.Ogre, MTGLib.MTGObject.SubType.Warrior }
};
var crab = new MTGLib.MTGObject.BaseCardAttributes()
{
name = "Wishcoin Crab",
manaCost = new ManaCost(
3, ManaSymbol.Blue
),
power = 2,
toughness = 5,
cardTypes = new HashSet<MTGObject.CardType> { MTGLib.MTGObject.CardType.Creature },
subTypes = new HashSet<MTGObject.SubType> { MTGLib.MTGObject.SubType.Crab }
};
And now for static abilities! Here is a card with two static abilities that boost creature stats based on colour. The new condition
property of a modification allows for the power boost to only apply for red creatures that are on the battlefield.
// Make the +1/+0 static ability
var redStatic = new StaticAbility(new PowerMod
{
value = 1, // value of 1...
operation = Modification.Operation.Add, // ... added to the original
condition = (obj) =>
{
// Is this object a creature?
if (!obj.attr.cardTypes.Contains(MTGObject.CardType.Creature))
return false;
// Is this object red?
if (!Util.ColorHas(obj.identity, Color.Red))
return false;
// Is this object on the battlefield?
if (obj.FindMyZone() != MTG.Instance.battlefield)
return false;
return true;
}
});
// Make the +0/+1 static ability
var whiteStatic = new StaticAbility(new ToughnessMod
{
value = 1,
operation = Modification.Operation.Add,
condition = (obj) =>
{
if (!obj.attr.cardTypes.Contains(MTGObject.CardType.Creature))
return false;
if (!Util.ColorHas(obj.identity, Color.White))
return false;
if (obj.FindMyZone() != MTG.Instance.battlefield)
return false;
return true;
}
});
// Haven't got activated abilities yet. Haven't even got the stack yet đ
var legionsInitiative = new MTGObject.BaseCardAttributes()
{
name = "Legion's Initiative",
manaCost = new ManaCost(ManaSymbol.Red, ManaSymbol.White),
cardTypes = new HashSet<MTGObject.CardType> { MTGObject.CardType.Enchantment },
staticAbilities = new List<StaticAbility> { redStatic, whiteStatic }
};
After making the test program randomly dump some creatures on the battlefield (as well as this new enchantment), I made it print out the power and toughness of each creature on the battlefield to test it works.
Success! The ogres have +1/+0 as they are red, while the crabs are at their untouched 2/5. Lets try and make it more complex.
L A Y E R S
Layers are an aspect of magic rules that dictate the order that modifications are applied to an object. During normal play they rarely come up, so they are rarely heard of and often a source of confusion. I need to make sure that all modifications are applied in the right order:
public void CalculateAttributes()
{
ResetAttributes();
// TODO - Make sure effects are applied in timestamp order
// TODO - How the hell does dependency work
var allMods = MTG.Instance.AllModifications;
List<int> indexes = new List<int>(Enumerable.Range(0, allMods.Count));
//613.1a Layer 1: Rules and effects that modify copiable values are applied.
//613.2a Layer 1a: Copiable effects are applied
//613.2b Layer 1b: Face-down spells and permanents have their characteristics modified as defined in rule 707.2.
//613.1b Layer 2: Control - changing effects are applied.
for (int i = indexes.Count-1; i >= 0; i--) {
var mod = allMods[indexes[i]];
if (mod is ControllerMod cast)
{
attributes.controller = cast.Modify(attributes.controller, this);
}
}
//613.1c Layer 3: Text - changing effects are applied.See rule 612, âText - Changing Effects.â
//613.1d Layer 4: Type - changing effects are applied.These include effects that change an objectâs card type, subtype, and / or supertype.
//613.1e Layer 5: Color - changing effects are applied.
for (int i = indexes.Count-1; i>=0; i--)
{
var mod = allMods[indexes[i]];
if (mod is ColorMod cast)
{
attributes.color = cast.Modify(attributes.color, this);
}
}
//613.1f Layer 6: Ability - adding effects, keyword counters, ability-removing effects, and effects that say an object canât have an ability are applied.
//613.1g Layer 7: Power - and / or toughness - changing effects are applied
//613.4a Layer 7a: Effects from characteristic - defining abilities that define power and / or toughness are applied.See rule 604.3.
//613.4b Layer 7b: Effects that set power and / or toughness to a specific number or value are applied.Effects that refer to the base power and/ or toughness of a creature apply in this layer.
for (int i = indexes.Count-1; i >= 0; i--)
{
var mod = allMods[indexes[i]];
if (mod.operation != Modification.Operation.Override)
continue;
if (mod is PowerMod cast)
{
attributes.power = cast.Modify(attributes.power, this);
indexes.RemoveAt(i);
continue;
}
if (mod is ToughnessMod cast2)
{
attributes.toughness = cast2.Modify(attributes.toughness, this);
indexes.RemoveAt(i);
continue;
}
}
//613.4c Layer 7c: Effects and counters that modify power and / or toughness(but donât set power and / or toughness to a specific number or value) are applied.
for (int i = indexes.Count-1; i >= 0; i--)
{
var mod = allMods[indexes[i]];
if (!(mod.operation == Modification.Operation.Add || mod.operation == Modification.Operation.Subtract))
continue;
if (mod is PowerMod cast)
{
attributes.power = cast.Modify(attributes.power, this);
indexes.RemoveAt(i);
continue;
}
if (mod is ToughnessMod cast2)
{
attributes.toughness = cast2.Modify(attributes.toughness, this);
indexes.RemoveAt(i);
continue;
}
}
}
It was quite finicky to work out how to loop through each modification. I have to remove modifications that have already been applied from the list, which involves iterating through everything in awkward backwards for loops so everything doesn't break.
There's still a bunch of layers that I haven't implemented. I'm dreading layer 6 the most - what will adding/removing abilities actually involve?
I also made a test enchantment that makes all permanents red. This should make the crabs red - which should allow them to benefit from the power boost.
var allPermsAreRed = new StaticAbility(new ColorMod
{
value = Color.Red,
operation = Modification.Operation.Override,
condition = (obj) =>
{
if (obj.FindMyZone() != MTG.Instance.battlefield)
return false;
return true;
}
});
var redEnchantment = new MTGObject.BaseCardAttributes()
{
name = "Red Enchantment",
manaCost = new ManaCost(1, ManaSymbol.Red, ManaSymbol.Red),
cardTypes = new HashSet<MTGObject.CardType> { MTGObject.CardType.Enchantment },
staticAbilities = new List<StaticAbility> { allPermsAreRed }
};
When that enchantment is on the battlefield...
Everything seems to work! Pretty pleased that some stuff seems to be coming together.
There's still a lot of complexity with the layer system that I haven't implemented. For example, when multiple objects are applying the same effect, the "newer" one gets applied last. There is also a "dependency" system that overrides this when one ability "depends" on another. This timestamp system is very strictly defined, and the dependency system really isn't. CR 613. Interaction of Continuous Effects
I have no idea how much of this I will end up implementing, especially as interactions where it matters so rarely come up. Ultimately I can choose to just ignore rules like that and no-one can stop me I guess :). I'll enjoy the flexibility of working on my own projects while I can.
Next time I'll try to get some sort of a turn structure working, and maybe something vaguely resembling an actual game. If I'm lucky.