It is currently 18 Apr 2024, 12:43
   
Text Size

Rulemods: What they are, and the awesome stuff they can do.

by Incantus

Moderator: CCGHQ Admins

Rulemods: What they are, and the awesome stuff they can do.

Postby MageKing17 » 27 Jun 2010, 04:36

I'm going to do something uncharacteristic of me and explain one of the not-well-explored features of Incantus, "rulemods".

What is a rulemod?
A rulemod is nothing but a .py file; a python script, nothing more, nothing less. However, being placed in the data/rulemods folder will grant this python script incredible power: the GameKeeper will execute its code before loading cards, and in the same environment cards use, meaning it can define new things (or change existing things) that cards can then go on and use. Obviously, this is a very powerful feature, and also one not terribly useful for real cards, since their mechanics should be defined in the regular source code.

So why would you use a rulemod?
Well, as I may have mentioned, I enjoy making custom cards; and if there's one thing custom card-makers like more than twisting an existing mechanic to suit their purposes, it's making entirely new mechanics. You can use rulemods to define totally new keywords, helper functions (like the existing chroma and domain functions), and even entirely new card types.

So how would I make and use a rulemod, then?
Well, first we need something we want to make a rulemod out of. Since I've recently been working on one of my custom card sets, let's take a bit of a difficult card out of one and see if we can make it easier with a rulemod.

Here's one of my custom cards:
Fraelin, Ancient One.jpg
If you please, just focus on the rules text; the rest is more-or-less random.


Now, you may be thinking, "That's easy! It's just chroma without a specific color." Well, that's what makes it difficult; chroma is simple because it looks for a specific color. It doesn't have to worry about where a hybrid symbol begins or ends because it just has to count the occurrences of that color. With this bad boy, however, we do need to worry about such complexities. So, without further ado, let's kick in the rulemod!

Code: Select all
global count_colored_symbols

import characteristics

def count_colored_symbols(cost):
    if isinstance(cost, characteristics.stacked_variable): cost = cost.current
    if not isinstance(cost, (MultipleCosts, ManaCost, str)): return 0
    if isinstance(cost, MultipleCosts):
        costs = cost.costs[:]
        costs = cost.consolidate(costs)
        if isinstance(costs[0], ManaCost): cost = costs[0].cost
        else: return 0
    elif isinstance(cost, ManaCost): cost = cost.cost
    total = 0
    in_hybrid = False
    current_hybrid = 0
    max_hybrid = 0
    for sym in cost:
        if sym in '({':
            in_hybrid = True
        elif sym == '/':
            current_hybrid = 0
        elif sym == ')}':
            in_hybrid = False
            total += max_hybrid
            max_hybrid = 0
            current_hybrid = 0
        if sym in "WUBRG":
            if in_hybrid:
                current_hybrid += 1
                if current_hybrid > max_hybrid: max_hybrid = current_hybrid
            else:
                total += 1
    return total

So what does that all mean? Let's take it step-by-step...

Code: Select all
global count_colored_symbols
The global environment of every rulemod is the CardEnvironment, which means that if we want a card to be able to access something, we need to make it global. Here, we're saying, "When I make something called 'count_colored_symbols', put it in the card environment."

Code: Select all
import characteristics
This is a tricky bit. Cards aren't normally supposed to do anything directly with characteristic objects, so they're not already imported into CardEnvironment. As a result, we'll have to do it ourselves. Why do we need characteristics? Well, you'll see in a minute.

Code: Select all
def count_colored_symbols(cost):
Here, we're actually making the aforementioned count_colored_symbols. We're making it a function that takes a single non-optional argument.

Code: Select all
    if isinstance(cost, characteristics.stacked_variable): cost = cost.current
This is why we imported "characteristics" earlier; we need to see if we're dealing with a stacked_variable (which, at runtime, card costs are) and, if we are, retrieve its current value for us to examine.

Code: Select all
    if not isinstance(cost, (MultipleCosts, ManaCost, str)): return 0
With this line, we declare that anything that can't be interpreted as a mana cost need not apply; there's no sense trying to count the colored mana symbols in a SacrificeCost or DiscardCost (or, even more nonsensically, in a TriggeredAbility or even a dict).

Code: Select all
    if isinstance(cost, MultipleCosts):
If our cost is a MultipleCosts object, it could very well contain a ManaCost we can count the symbols of, so we'll need to do some special logic on it...

Code: Select all
        costs = cost.costs[:]
        costs = cost.consolidate(costs)
Here, we're extracting the list of costs contained by the MultipleCosts object, and then consolidating them (the MultipleCosts.consolidate() method combines ManaCosts and puts them at the front of the list).

Code: Select all
        if isinstance(costs[0], ManaCost): cost = costs[0].cost
        else: return 0
And now we say "if our consolidated costs begin with a ManaCost, grab its cost string. Otherwise, we got nothin'."

Code: Select all
    elif isinstance(cost, ManaCost): cost = cost.cost
And finally, turn a regular old ManaCost into its cost string so we can go through it character-by-character.

Code: Select all
    total = 0
    in_hybrid = False
    current_hybrid = 0
    max_hybrid = 0
These variables will help us parse the symbols in the cost. "total" is the number of colored mana symbols we've found, "in_hybrid" is whether or not we're currently inside of a hybrid symbol (denoted with parentheses or curly braces), "current_hybrid" is the number of colored mana symbols we've found in this section of the hybrid symbol, and "max_hybrid" is the most colored mana symbols in any part of the hybrid symbol (so a "(2/W)" symbol would have 0 and 1, leading to a max_hybrid of 1, counting it as a colored symbol).

Code: Select all
    for sym in cost:
Walk through each character in the string.

Code: Select all
        if sym in '({':
            in_hybrid = True
We've now entered a hybrid symbol and need to keep track of hybrid logic instead of regular symbol logic.

Code: Select all
        elif sym == '/':
            current_hybrid = 0
We've switched to another possibility of the hybrid symbol, and need to reset the count of colored symbols in this section.

Code: Select all
        elif sym == ')}':
            in_hybrid = False
            total += max_hybrid
            max_hybrid = 0
            current_hybrid = 0
When exiting a hybrid symbol, add our highest colored symbol value to the total, and reset our counting variables (which could also have been done when entering a symbol, it doesn't matter either way).

Code: Select all
        if sym in "WUBRG":
We found a colored symbol! Hurrah!

Code: Select all
            if in_hybrid:
                current_hybrid += 1
                if current_hybrid > max_hybrid: max_hybrid = current_hybrid
            else:
                total += 1
If we're in a hybrid symbol, increase the current colored symbol count and, if neccessary, the maximum colored symbol count. Otherwise, just increase the total directly.

Code: Select all
    return total
Finally, after all that work, we need to make sure the caller gets the value they asked for in the beginning.

And there you have it! We have successfully added a function to the CardEnvironment we can use to implement Fraelin. Let's code his card, now.

Code: Select all
name = 'Fraelin, Ancient One'
cost = '4UB'
supertypes = Legendary
types = Creature
subtypes = Vampire, Wizard
power = 3
toughness = 3
text = ["2UB, T: Scry 2, then put the top card of your library into your graveyard. Put a +1/+1 counter on ~ for each colored mana symbol in that card's mana cost."]

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

@activated(txt=text[0])
def ability():
    def effects(controller, source):
        cost = yield ManaCost("2UB") + TapCost()
        target = yield NoTarget()
        controller.scry(2)
        yield
        for card in controller.library.top(1):
            counters = count_colored_symbols(card.cost)
            card.move_to("graveyard")
        yield
        source.add_counters(PowerToughnessCounter(1, 1), number=counters)
        yield
    return effects
abilities.add(ability)
--------------- Fraelin, Ancient One
As you can see, the card itself is straightforward now that the complicated calculations have been relegated to a special function. Now any other time we need a card to count the colored mana symbols in a mana cost, we just invoke count_colored_symbols(), kick back, and relax.

The only step left to put this card in Incantus is making the image 200x285 pixels. Let me run that image through my handy-dandy image-resizing tool and see what we come up with...
Fraelin_Ancient_One.jpg
He's travel-sized, for your convenience.
Oh yeah. We're in business now.

Fraelin_Ancient_One_custom_card.zip
And here's a ready-to-download-and-use archive with all this stuff ready to roll.
(22.32 KiB) Downloaded 509 times
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