It is currently 16 Apr 2024, 19:45
   
Text Size

Exploring a new card format

by Incantus

Moderator: CCGHQ Admins

Exploring a new card format

Postby MageKing17 » 23 Jun 2010, 01:45

Incantus and I have talked a bit about changing the card format so that abilites are defined as classes. For those of you who haven't been watching the evolution of the card format (and those who want a refresher), cards used to look like this:
Code: Select all
name = 'Chainer, Dementia Master'
cardnum = 0
expansion = ''
type = characteristic('Creature')
supertype = characteristic('Legendary')
subtypes = characteristic(['Human', 'Minion'])
cost = '3BB'
color = characteristic(['B'])
text = ['Nightmare creatures get +1/+1.', 'BBB, Pay 3 life: Put target creature card from a graveyard into play under your control. That creature is black and is a Nightmare in addition to its other creature types.', 'When Chainer, Dementia Master leaves play, remove all Nightmares from the game.']

out_play_role.abilities = [CastPermanentSpell(card, cost)]

subrole = Creature(5, 3)

in_play_role = Permanent(card, subrole)

#################################

subrole.abilities = [ActivatedAbility(card,
   cost=MultipleCosts([ManaCost("BBB"),LifeCost(3)]),
   target=Target(target_types=isCreatureType, zone="graveyard", player_zone=None),
   effects=[ChangeZoneToPlay(from_zone="graveyard"),
      ModifyColor(characteristic("B"), expire=False),
      ModifySubtype(additional_characteristic("Nightmare"), expire=False)
   ]
)]
subrole.triggered_abilities=[TriggeredAbility(card,
   trigger=LeavingTrigger(zone="play"),
   match_condition=SelfMatch(card),
   ability=Ability(card, target=AllPermanentTargets(target_types=isCreature.with_condition(lambda c: c.subtypes == "Nightmare")),
      effects=ForEach(ChangeZone(from_zone="play", to_zone="removed"))
   )
)]
subrole.static_abilities = [PermanentTrackingAbility(card,
   condition = isCreature.with_condition(lambda c: c.subtypes == "Nightmare"),
   events = SubtypeModifiedEvent(),
   effects = AugmentPowerToughness(power=1,toughness=1,expire=False)
)]

#################################
A bit of a mess, I know (there's a reason we changed).

After a while, the card format "evolved" into this slightly better format:
Code: Select all
name = 'Chainer, Dementia Master'
cardnum = 0
expansion = ''
type = characteristic('Creature')
supertype = characteristic('Legendary')
subtypes = characteristic(['Human', 'Minion'])
cost = '3BB'
color = characteristic(['B'])
text = ['Nightmare creatures get +1/+1.', 'BBB, Pay 3 life: Put target creature card from a graveyard into play under your control. That creature is black and is a Nightmare in addition to its other creature types.', 'When Chainer, Dementia Master leaves play, remove all Nightmares from the game.']

play_spell = CastPermanentSpell(card, cost)

in_play_role = Permanent(card, Creature(3, 3))

#################################

abilities.add(PermanentTrackingAbility(card,
   condition = isCreature.with_condition(lambda c: c.subtypes == "Nightmare"),
   events = SubtypeModifiedEvent(),
   effects = AugmentPowerToughness(power=1,toughness=1,expire=False),
   txt=text[0]
))

abilities.add(ActivatedAbility(card,
   cost=MultipleCosts([ManaCost("BBB"),LifeCost(3)]),
   target=Target(target_types=isCreatureType, zone="graveyard", player_zone=None),
   effects=[ChangeZoneToPlay(from_zone="graveyard"),
      ModifyColor(characteristic("B"), expire=False),
      ModifySubtype(additional_characteristic("Nightmare"), expire=False)],
   txt=text[1]
))

abilities.add(TriggeredAbility(card,
   trigger=LeavingTrigger(zone="play"),
   match_condition=SelfMatch(card),
   ability=Ability(card, target=AllPermanentTargets(target_types=isCreature.with_condition(lambda c: c.subtypes == "Nightmare")),
      effects=ForEach(ChangeZone(from_zone="play", to_zone="removed"))
   ),
   txt=text[2]
))

#################################
An improvement to be sure, but still none-too-great.

Finally, a massive change occurred when abilities changed to being defined as decorators:
Code: Select all
name = 'Chainer, Dementia Master'
cardnum = 0
expansion = ''
types = characteristic('Creature')
supertypes = characteristic('Legendary')
subtypes = characteristic('Minion', 'Human')
cost = '3BB'
color = characteristic('B')
power = 3
toughness = 3
text = ['Nightmare creatures get +1/+1.', 'BBB, Pay 3 life: Put target creature card from a graveyard into play under your control. That creature is black and is a Nightmare in addition to its other creature types.', 'When Chainer, Dementia Master leaves play, remove all Nightmares from the game.']

play_spell = play_permanent()

#################################

@static_tracking(txt=text[0], events=(TypesModifiedEvent(), SubtypesModifiedEvent()))
def ability():
    def condition(source, card):
        return isCreature(card) and card.subtypes == "Nightmare"
    def effects(source, card):
        yield card.augment_power_toughness_static(1, 1)
    return condition, effects
abilities.add(ability)

@activated(txt=text[1])
def ability():
    def effects(controller, source):
        payment = yield ManaCost('BBB') + LifeCost(3)
        target = yield Target(isCreatureCard, zone="graveyard")
        # Code for effect
        def enter_play_black_nightmare(self):
            self.color.set('B')
            self.subtypes.add('Nightmare')
        expire = CiP(target, enter_play_black_nightmare, txt='~ - Target is black and a Nightmare in addition to its other creature types')
        target = target.move_to(controller.play)
        expire()
        yield # I CAN'T BELIEVE I FORGOT THE YIELD
    return effects
abilities.add(ability)

@triggered(LeaveTrigger("play"), txt=text[2])
def ability():
    def effects(controller, source, card):
        target = yield NoTarget()
        # Code for effect
        for nightmare in controller.play.get(isPermanent.with_condition(lambda c: c.subtypes == "Nightmare"), all=True):
            nightmare.move_to("removed")
        yield
    return source_match, effects
abilities.add(ability)
This was a tremendous change, and the engine continued to expand (in fact, there are a number of minor alterations that occurred in the format, but I didn't want to have to go digging for a truly outdated version of a generator-based Chainer, Dementia Master.

This is what Chainer, Dementia Master's code looks like now:
Code: Select all
name = 'Chainer, Dementia Master'
cost = '3BB'
types = Creature
supertypes = Legendary
subtypes = Human, Minion
power = 3
toughness = 3
text = ['Nightmare creatures get +1/+1.', 'BBB, Pay 3 life: Put target creature card from a graveyard onto the battlefield under your control. That creature is black and is a Nightmare in addition to its other creature types.', 'When ~ leaves the battlefield, exile all Nightmares.']

#################################

@static_tracking(events=(TypesModifiedEvent(), SubtypesModifiedEvent()), txt=text[0])
def ability():
    def condition(source, card):
        return isCreature(card) and card.subtypes == Nightmare
    def effects(source, card):
        yield card.augment_power_toughness(1, 1)
    return condition, effects
abilities.add(ability)

@activated(txt=text[1])
def ability():
    def effects(controller, source):
        cost = yield ManaCost('BBB') + LifeCost(3)
        target = yield Target(isCreatureCard, zone="graveyard")
        # Code for effect
        def enter_battlefield_black_nightmare(self):
            self.color.set(Black)
            self.subtypes.add(Nightmare)
        expire = CiP(target, enter_battlefield_black_nightmare, txt='~ - Target is black and a Nightmare in addition to its other creature types')
        newcard = target.move_to(controller.battlefield)
        expire()
        yield
    return effects
abilities.add(ability)

@triggered(txt=text[2])
def ability():
    def effects(controller, source, card):
        target = yield NoTarget()
        # Code for effect
        for nightmare in controller.battlefield.get(isPermanent.with_condition(lambda c: c.subtypes == Nightmare), all=True):
            nightmare.move_to("exile")
        yield
    return LeaveTrigger("battlefield", source_match), effects
abilities.add(ability)
Another "minor" upgrade, you can tell that things are different, but not massively so.

Now, Incantus and I are considering another major change: class-based ability definitions. Here's a mockup of how Chainer's abilities might look under the new system:
Code: Select all
@static_tracking()
class ability:
    events = lambda self: (TypesModifiedEvent(), SubtypesModifiedEvent())
    def condition(self, card):
        return isCreature(card) and card.subtypes == Nightmare
    def effects(self, card):
        return card.augment_power_toughness(1, 1)
abilities.add(ability)

@activated()
class ability:
    cost = lambda self: ManaCost('BBB') + LifeCost(3)
    target = lambda self: Target(isCreatureCard, zone="graveyard")
    def resolve(self):
        def enter_battlefield_black_nightmare(self):
            self.color.set(Black)
            self.subtypes.add(Nightmare)
        expire = CiP(target, enter_battlefield_black_nightmare, txt='~ - Target is black and a Nightmare in addition to its other creature types')
        newcard = target.move_to(controller.battlefield)
        expire()
abilities.add(ability)

@triggered()
class ability:
    triggers = lambda self: LeaveTrigger("battlefield", source_match)
    def resolve(self):
        for nightmare in controller.battlefield.get(isPermanent.with_condition(lambda c: c.subtypes == Nightmare), all=True):
            nightmare.move_to("exile")
abilities.add(ability)
You may have noticed the lack of "yield" statements. The new resolve() function is not a generator; instead, in order to send timesteps, you have to actually tell it to send a timestep, by calling "self.timestep()". This makes single-step spells and abilities easier, whilst making multi-part effects more explicit about breaks. Also, I didn't get a chance to show it here, but resolve() blocks also don't have (controller, source) arguments because the controller and source are properties of the instance of the ability-class (as well as target and cost, so "self.source.deal_damage(self.target)", for instance).

As you can see, all the various types of abilities should still work, and in fact the engine might not need to change that much to accommodate it (and we think things like Feral Hydra, where someone other than the card's controller activates an ability, would be much easier). It's a big change, though, and since it's not a simple issue, I thought I'd ask you all for thoughts and opinions.
User avatar
MageKing17
Programmer
 
Posts: 473
Joined: 12 Jun 2008, 20:40
Has thanked: 5 times
Been thanked: 9 times

Re: Exploring a new card format

Postby Marek14 » 23 Jun 2010, 04:53

I think that if it improves the system, you should go for it :)
Marek14
Tester
 
Posts: 2759
Joined: 07 Jun 2008, 07:54
Has thanked: 0 time
Been thanked: 296 times

Re: Exploring a new card format

Postby MageKing17 » 23 Jun 2010, 06:28

Marek14 wrote:I think that if it improves the system, you should go for it :)
Such deeply insightful commentary. ;)

The real question is, of course, whether there are any features people would like to make sure get in now, while it's still in the planning stages, rather than waiting until after it's already been coded, and very difficult (if not impossible) to change.
User avatar
MageKing17
Programmer
 
Posts: 473
Joined: 12 Jun 2008, 20:40
Has thanked: 5 times
Been thanked: 9 times

Re: Exploring a new card format

Postby juzamjedi » 23 Jun 2010, 14:35

I think it's safe to say that we all want a rules engine that can handle the most situations possible. One that is flexible so that most (or all!) of the current abilities in Magic could be implemented and crazy new abilities have a reasonable shot at being added to the engine.

I'm not a Python expert at all and 90% or more of the Python code I have ever seen has been Incantus. I'm looking at the code for Chainer above and I don't see a significant difference from last version to current. I don't see timestep() events in the example would this happen as part of resolve() ?

With the current state of the card code base I don't think making this change now would cause a large issue. There haven't been too many cards coded with the "current" system anyway.

Can we copy spells on the stack now? :)

Mana types and restrictions would be nice to have in the engine (snow mana, Mishra's Workshop, Ancient Ziggurat, etc.).

Also more generally have you looked at keyword abilities that have not been implemented yet and given thought to how they would work (or just add them to the engine on the next update)?
juzamjedi
Tester
 
Posts: 575
Joined: 13 Nov 2008, 08:35
Has thanked: 6 times
Been thanked: 8 times

Re: Exploring a new card format

Postby MageKing17 » 23 Jun 2010, 19:59

juzamjedi wrote:I think it's safe to say that we all want a rules engine that can handle the most situations possible. One that is flexible so that most (or all!) of the current abilities in Magic could be implemented and crazy new abilities have a reasonable shot at being added to the engine.
Well, yeah. It's easier said than done, though. ;)

juzamjedi wrote:I'm not a Python expert at all and 90% or more of the Python code I have ever seen has been Incantus. I'm looking at the code for Chainer above and I don't see a significant difference from last version to current. I don't see timestep() events in the example would this happen as part of resolve() ?
Yes. Currently, the effects() functions are generators, which yield back to the rules engine to send timesteps. With resolve() functions, they won't be generators anymore, so they'll send the timesteps directly. Here's an example of "draw a card, then discard a card, in the old system, and the new:
Code: Select all
OLD:
    def effects(controller, source):
        cost = yield blah blah
        target = yield NoTarget()
        controller.draw()
        yield
        controller.force_discard()
        yield

NEW:
    cost = lambda self: blah blah
    def resolve(self):
        self.controller.draw()
        self.timestep()
        self.controller.force_discard()
As you can see, it takes up a bit less space and is more easily extensible.

juzamjedi wrote:With the current state of the card code base I don't think making this change now would cause a large issue. There haven't been too many cards coded with the "current" system anyway.
Indeed, this is an ideal time to make the transition, which is why I want everyone's input on it.

juzamjedi wrote:Can we copy spells on the stack now? :)
This is one of the reasons this new system is being considered. Copying spells on the stack should be easier if we do this right. So should entwine.

juzamjedi wrote:Mana types and restrictions would be nice to have in the engine (snow mana, Mishra's Workshop, Ancient Ziggurat, etc.).
Mana restrictions are almost certainly going to have to wait until individual mana points are represented by their own objects. That being said, snow mana may be possible anyway, by splitting the mana pool into two halves. Which won't be pretty. On second thought, maybe it'd be best to wait. :P

juzamjedi wrote:Also more generally have you looked at keyword abilities that have not been implemented yet and given thought to how they would work (or just add them to the engine on the next update)?
Well, I know that rebound should be possible (I just didn't have time for it when I was doing madness and flashback), if a bit complicated. Off the top of my head, bloodthirst and bloodthirst X should also be pretty easy. The problem is the real problem keywords, like phasing and morph, and they're not gonna be implemented for a while. I have no hope of getting morph to work right without Incantus's help, at any rate, and I doubt phasing will ever work (but I suppose I could be proven wrong on that). Beyond that, if you look at the source code (specifically, engine/Ability/UnimplementedAbility.py), you should be able to figure out which abilities are possible using existing techniques and which ones require an extensive overhaul. I'll give them a once-over myself before I work on the next version (be it v0.7.1b or v0.7.2 or whatever).

EDIT:As an example of something the new system can do that the old can't, here's how the Feral Hydra ability would work:
Code: Select all
@activated()
class ability:
    cost = lambda self: ManaCost('3')
    def playable_by(self, player): return True
    def resolve(self):
        self.source.add_counters(PowerToughnessCounter(1, 1))
abilities.add(ability)
Short, sweet, to the point, and impossible in the current system.
User avatar
MageKing17
Programmer
 
Posts: 473
Joined: 12 Jun 2008, 20:40
Has thanked: 5 times
Been thanked: 9 times

Re: Exploring a new card format

Postby Huggybaby » 25 Jun 2010, 02:29

That's the main reason to start over isn't it, to be able to do things the current system can't. Is there a list of functions the current system cannot perform? And are they all possible in the new one?
User avatar
Huggybaby
Administrator
 
Posts: 3205
Joined: 15 Jan 2006, 19:44
Location: Finally out of Atlanta
Has thanked: 696 times
Been thanked: 594 times

Re: Exploring a new card format

Postby MageKing17 » 25 Jun 2010, 04:51

Huggybaby wrote:That's the main reason to start over isn't it, to be able to do things the current system can't. Is there a list of functions the current system cannot perform? And are they all possible in the new one?
A comprehensive list of things we can't do is pretty much impossible, but on a more limited scale, we never bothered to get around to making a note whenever we ran into something we couldn't do. Whenever we get around to modifying our to-do lists, it's pretty much the sort of stuff I've already mentioned (entwine, copying spells on the stack, activating abilities of permanents you don't control, et cetera).
User avatar
MageKing17
Programmer
 
Posts: 473
Joined: 12 Jun 2008, 20:40
Has thanked: 5 times
Been thanked: 9 times


Return to Incantus

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