2
\$\begingroup\$

I'm trying to recreate a simple card game. Each played card has an action (draw extra cards, peek at draw stack, eliminate an opponent card, etc). Once played, it goes on a stack, and the card effect is executed in order.

Cards added by effects are added at the end of this stack as well, and executed once it is their turn.

Each player also has a different unique influence on exactly one type of cards that changes the effect (enhances it like draw 4 instead of 2 or taking cards instead of trashing them to graveyard).

So far a card has a simple interface with value, type and doAction. While I could on each card do a check if a player influence would take effect, this seems like a hard coded noodle salad and makes it difficult to add new cards or influences. And more than once influence can take effect on the same card (for example, one effect could be steal 2 instead of 1 and the second effect, you can steal only a certain type of card from a second influence instead of free choice)

I'm looking for a way to react on a method call (played card that gets executed) and change it accordingly. I'm investigating using delegates for this purpose.

\$\endgroup\$
  • \$\begingroup\$ events and delegates are mostly for observer design pattern. you use them when you want to do multiple task cleanly with one call. using a delegate always depends on you approach \$\endgroup\$ – virtouso Aug 16 at 10:26
  • \$\begingroup\$ How have you tried applying delegates to solve this problem so far? Did it work? If so, great! You don't need us at all. If it didn't work yet, where exactly did you get stuck or run into a problem that you need help solving? \$\endgroup\$ – DMGregory Aug 16 at 10:52
  • \$\begingroup\$ Possible duplicate of Design of a turn-based game where actions have side-effects \$\endgroup\$ – Pikalek Aug 16 at 14:15
  • \$\begingroup\$ How many unique card powers are you talking about? Strategies that are reasonable for many cards (scripting languages, ECS, etc) are over engineered for small scale instances... \$\endgroup\$ – Pikalek Aug 16 at 14:21
  • 1
    \$\begingroup\$ @Pikalek we are talking about 10 base cards and 50 modifier (player powers). The base cards are always in the game, for each player a modifier is picked and that one is affecting exactly one base card (but different players can affect the same base card with a different result) \$\endgroup\$ – Zibelas Aug 17 at 7:58
2
\$\begingroup\$

As the comments have indicated, delegates may be your saviour, but let me add a little context and maybe a few extra pointers.

delegates are nice because you can form a subscriber system whereby the player can be subscribed to the card with some functions X that executes the desired actions on the card.

however delegates may not be the perfect solution as your game grows, while functionally similar, code may become more readable in a situation where cards could do anything like... Switching owners, reversing effects, or affecting other cards... if you format this as a list of Func that run on the card before it's effect is executed then you can easily encapsulate further for more functionality.

So i am proposing that you may prefer a solution like

class Card{

     public object value;
     public object type;
     public list<action<Card>> ExtraEffects;
     public void Execute(){
          //create a new card that can be modified however you like with the funcs without modifying the base card
          var cloneCard = Clone();
          foreach(var fun in Modifiers)
               cloneCard = fun(cloneCard);
          cloneCard.ExecuteModified();
     }
     public void ExecuteModified(){
          //do your thing here.
     }
     public list<Func<card, card>> Modifiers;
     public void ClearModifiers(){
          //clear all modifiers.
     }
     public Card Clone(){
          //clone code
     }

this is really good because it scales easily. if at any point you decide you want to keep the thing that causes an effect referenced then you can just encapsulate func with something else next to it like a reference to thing that caused the effect.

struct Effect{
    Func<Card, Card> Modifier;
    object Source;//like the player, field effects, enemy effects, other card's effects
}

class Card{
    public List<Effect> Modifiers;
    ....
}

This is as opposed to delegates and events where it becomes difficult to track each subscription as opposed to just running all of them.

for instance you could use this architecture to add functions like

public void ClearPlayerModifications(){
    Modifiers = Modifiers.Where(x=>x.Source != Player)
}

games where a lot of dynamic effects happen can grow wildly really easily, its worth it in my experience to manage it this way instead.

this is as opposed to growing the class in ways like this

public void ClearPlayerModifications(){
    //a likely implementation with events and delegates. really error prone for a system where effects could change every turn.
    Modificatiers.Clear();
    Modifications.AddEverythingThatShouldStillBeThere();
}
\$\endgroup\$
2
\$\begingroup\$

Delegates can be used to achieve what you're doing but the code here will most likely not 'just work' with what you already have. In addition, I've had to make several assumptions about how things work / what you have described (see assumptions at the end).

Cards

Given your description, I imagine the interface your cards use to look something like this:

interface ICard {
    CardType cardType {get;}
    int amount {get;} // Or whichever number type is appropriate
    void DoCardAction();
}

You said that player effects could completely change the action of your cards. A delegate property instead of an interface method for the card's action would support this replacement.

interface ICard {
    CardType cardType {get;}
    int amount {get;} // Or whichever number type is appropriate
    Action doCardAction {get; set;} // The delegate
}

Having a card without any action is probably not what you want. To avoid that, the implementing class would then assign the 'normal' action to this property on initialisation / construction, e.g.:

class CardImplementation : ICard {
    // etc...
    void CardAction() {/* etc...*/}
    public CardImplementation() {
        doCardAction = CardAction;
        //etc...
    }
}

Influeces

With this new delegate-centred interface, modifying the card's base effect would be replacing it with a delegate to a different method. We can also incorporate another delegate for modifying the 'amount' values so that different players can effect different modifications without the need for subclassing.

class PlayerInfluence {
    CardType modifies;
    Func<int, int> modifier; // Or Func<X, X> where X is the data type of your card's 'amount'
    Action replacementAction;

    public bool CanModify(ICard target) {
        return modifies == target.cardType;
    }

    public void ModifyCard(ICard target) {
        // Replace the card's action if a replacement was supplied
        target.doCardAction = replacementAction ?? target.doCardAction;

        // Augment the target's amount property using a function, if supplied
        target.amount = modifier?.Invoke(target.amount) ?? target.amount;
    }
}

Delegate properties can hold lambdas, so creating a player with a specific effect is as simple as defining that effect on player creation. The example below creates an influence that will modify Peek cards by doubling the amount, but leaving the action unchanged.

PlayerInfluence peekingCardDoublingInfluence = new PlayerInfluence(CardType.Peek, (x) => x * 2, null);

Using a player's influence to modify a card

To bring this together, the routine for resolving an action from the stack would look something like:

currentEffect = effectStack.Pop(); // Get the next effect
if(currentPlayer.influence.CanModifyCard(currentEffect)) {
    currentPlayer.influence.Modify(currentEffect); // Modifiy the effect, if possible   
}
currentEffect.doCardAction(); // Run the effect

(some of the) Assumptions I'm making:

  • your card type is represented by an enumeration (called CardType)
  • you have implemented a constructor for Player as Player(CardType, Func<int, int>, Action)
  • The routine which controls stack resolution has access to the current player
  • Card effects are cloned on to the stack, rather than pointing to the same instance in the deck, if you want the modifications to be one-time-only
  • A card's default action is an instance method bound to ICard.doCardAction when the card is constructed / initialised.
  • If replacing a card's action, then ICard.amount is not relevant to the new action i.e. they are mutually-exclusive modifications
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.