How should events Cascade?

This is something of a best practices question, so here it goes:

I’m designing a multi-player Card Game (think a MTG clone). I’m setting up an event based architecture to try to keep my code as clean as possible, but I’m stuck on a very basic architecture question and could use some other perspectives:

When a player plays a card, what event should be triggered?

IE - we could invoke CardPlayedEvent on our Player, who played the card while passing in the card; or we could call it on the Card that was played, passing in the player. Lastly, we could call it on a GameManager class and pass both.

Secondly, should these events “cascade” or be invoked explicitly?

IE, let’s say I decide to raise the event on my GameManager; when Cards and Players come in to the universe they find the GameManager and subscribe to the event.

Alternatively, the GameManager can have a listener on it’s CardPlayedEvent event that raises the event on the Player who played the card; and subsequently the Player has a listener on it’s CardPlayedEvent that passes it down again to the card that was played.

Is there one approach that’s known to be problematic or likely to lead to issues down the road? Is there a generally accepted pattern I can follow?

Thanks for all the advice!

I don’t know the rules of your game, but Magic for example has a lot of play zones: your hand, the stack, the battlefield, the graveyard, exile, etc. I would try to mimic that as much as possible because cards entering and exiting all of those zones can trigger effects in the game, and having all of those events filtering through one monolithic GameManager is going to become a huge mess.

Any option sounds fine to me, but it depends on what information you need the events to pass.
Do you only need to know which player plays a card? Go with the first option.
Do you only need to know when a card is played? Go with the second option.
Do you need to know both the card that was played and the player who played it? Go with the third option.

Though for the third option, rather than creating a middle-man class between Player and Card, you could make the event static in either the Player or Card classes and have it pass both pieces of data.
For example:

public class Player {
   public static event Action<Player, Card> OnCardPlayed;

   void PlayCard(Card card) {
      //PlayCard logic...

      OnCardPlayed?.Invoke(this, card);
   }
}

Or the other way around:

public class Card {
   public static event Action<Card, Player> OnPlayed;

   void Play(Player player) {
      //Play logic...

      OnPlayed?.Invoke(this, player);
   }
}

This way, you don’t need to manage multiple subscriptions to multiple instance events for every Player and/or Card.

This approach could be useful if you need the Player to perform any logic before the Card knows it was played, but if it’s just a straight line to GameManager Event -> Player Event -> Card Event with nothing in-between, it might be just a bit more overhead.