Thursday, April 29, 2010

Cards - Two views on the same thing

I would really like to know if you can imagine of what two views I'm going to talk about. I guess it's more obvious for people who have thought about how to structure a magic application. If you like, leave a comment and state what's your background.

When you open a booster pack, how do you read cards? They have symbols and text, and of course you can read them and determine what they do. However, the card can't do anything, because you're not playing a game.
During a game, the card has a complex context that influences how you read it; For example, its color may have changed.

Humans can read a card outside the game and inside the game, but it's hard for a computer. The difference between these two views is very important and currently makes me some problems. A card outside the game (template), like the one from the booster, is a constant thing - it can't change between different copies. This means that multipe cards inside the game can share the same template, but those are then individuals. They have the same template, but that's it.
The same thought can be applied to abilities and spells, I think. Abilities and spells have another, even harder problem: Their effects. While cards just sit there, spells and abilities can directly influence the game, and what influence that is depends on how the spell/ability was played.

So what's the solution to the problem? Well, in a nutshell, the data structure for cards, spells and abilities is duplicated.

Outside of the game is CardTemplate and CardParts, inside the game is CardObject and CardCharacteristics. A CardCharacteristics is assigned to a CardObject is responsible for providing the characteristics, respecting effects and the layer system, but also taking into account the values "printed" on a CardParts object.

Outside the game is ActivatedAbility, inside the game is ActivateAction, PlayInformation and AbilityObject.
  • ActivatedAbility has a method getPlayInformation() which takes an ActivateAction and returns a PlayInformation.
  • The PlayInformation is the real worker of abilities and implements modes, targeting, costs, effects etc. It depends on the game, so an activated ability can only create the PlayInformation if a game is provided.
  • And to make things easier, the activate action has some additional infos like who played the ability and what card the ability is on (like I said, ActivatedAbility is outside the game, so it doesn't know which cards it is on. the ActivateAction knows).
All that happens under the surface. The ActivateAction retrieves the PlayInformation and does all the necessary things, like putting an AbilityObject onto the stack etc.

I hope that you had a chance to look into my program; I know that this is very hard to imagine without directly looking at the code, So I won't try to go into more detail unless you wish so ;)

6 comments:

Anonymous said...

More detail! It's very interesting. :)

Also, I see what you mean by two views(or rather,two forms?). I tried and got somewhat(not very) far in my own client a while ago, but it soon became a headache of complexity.

Silly Freak said...

thanks for your response! i don't have many readers for sure, so your wish is my command^^

sorting all this out was a headache for me, too. after all, there's another thing of importance except a working framework... making cards must be easy ;)
and I think i'm on the best way to that. i'll show you what it takes ;)

nantuko84 said...

few days ago I would ask you why you needed such two versions. why second version can't replace the first one.

but now I faced one issue with Kozilek, Butcher of Truth. in the code that came from Forge, card is loaded once again when it goes from battlefield to graveyard. it is made to reset all effects.
the problem appeared only for Kozilek as it registers event listeners for its last ability (to shuffle itself back to library). so when it is being reloaded, it registers itself once again.

actually it was solved by separating loading cards' parameters and theirs implementations.

could you think over any disadvantage of such approach?

and why do you think second version can't totally replace first version in your framework?
if you want, I can write what I maen in more detail

Silly Freak said...

I don't really understand what you mean. I know forge has a card class, but not how abilities are implemented...

my thought was that a printed card exists without a game, but calculations on cards, like determining power, can't be done without it.

Modifying the "printed" card directly has a big error potential, like i said in Traps in the rules system, so I didn't want to go that way. separating printed abilities from played abilities was similar: imagine that you play one ability in response to the same, and end up with overwritten targets and such...

nantuko84 said...

this is what I meant.
what are the cases that printed cards can't be used?
for your example:
why not just use cardfactory that creates new instance for the card (using clone interface you helped me with once).

Silly Freak said...

hmm... I can't imagine a case where printed cards *can* be used directly.
as I said, the layer system makes it very hard to modify the original values, because the interactions could corrupt them. Using the non-printed cards (which have support for that and require a game object) could work, but then the class is doing two things that are different from ground on, which isn't really OO. additionally, cards outside the game would have much more weight than they'd need.

In my program, cards can share the template, but the cards then are unique, so templates don't need to be cloned, and all the static, printed things are not saved at the card object in the first place.

In fact, getting templates works much like cloning. They are created once - by parsing plain text descriptions or from java code -, then serialized to a big zip file, and while the program is running, the templates can be deserialized from the zip file without doing plain text parsing at runtime

I hope that was in the direction you asked ;)