text: class CardChapel : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          if(s.players[s.player].hand.Length() > 0)
          {
              s.decision.SelectCards(c, 0, 4);
              if(decisionText) s.decision.text = "Select up to 4 cards to trash:";
              s.decision.cardChoices = s.players[s.player].hand;
          }
          else
          {
              if(logging) s.Log("has no cards to trash");
          }
      }
      bool CanProcessDecisions() const
      {
          return true;
      }
      void ProcessDecision(State &s, const DecisionResponse &response) const
      {
          for(Card *c : response.cards)
          {
              s.stack.PushEnd(new EventTrashCardFromHand(s.player, c));
          }
      }
  };
  
  class CardCellar : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          if(s.players[s.player].hand.Length() > 0)
          {
              s.decision.SelectCards(c, 0, s.players[s.player].hand.Length());
              if(decisionText) s.decision.text = "Select cards to discard:";
              s.decision.cardChoices = s.players[s.player].hand;
          }
          else
          {
              if(logging) s.Log("has no cards to discard");
          }
      }
      bool CanProcessDecisions() const
      {
          return true;
      }
      void ProcessDecision(State &s, const DecisionResponse &response) const
      {
          for(Card *c : response.cards)
          {
              s.stack.PushEnd(new EventDrawCard(s.player));
          }
          for(Card *c : response.cards)
          {
              s.stack.PushEnd(new EventDiscardCard(s.player, c, DiscardFromHand));
          }
      }
  };
  
  class CardMoneylender : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          if(s.players[s.player].hand.FindFirstIndex(s.data->baseCards.copper) != -1)
          {
              s.stack.PushEnd(new EventTrashCardFromHand(s.player, s.data->baseCards.copper));
              s.players[s.player].money += 3;
          }
          else
          {
              if(logging) s.Log("has no coppers to trash");
          }
      }
  };
  
  class CardWorkshop : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          s.decision.GainCardFromSupply(s, c, 0, 4);
          if(s.decision.cardChoices.Length() == 0)
          {
              s.decision.type = DecisionNone;
              if(logging) s.Log("has no cards to gain");
          }
      }
      bool CanProcessDecisions() const
      {
          return true;
      }
      void ProcessDecision(State &s, const DecisionResponse &response) const
      {
          s.data->players[s.decision.controllingPlayer].ledger.RecordBuy(response.cards[0]);
          s.stack.PushEnd(new EventGainCard(s.player, response.cards[0]));
      }
  };
  
  class CardFeast : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          s.decision.GainCardFromSupply(s, c, 0, 5);
          if(s.decision.cardChoices.Length() == 0)
          {
              s.decision.type = DecisionNone;
              if(logging) s.Log("has no cards to gain");
          }
      }
      bool CanProcessDecisions() const
      {
          return true;
      }
      void ProcessDecision(State &s, const DecisionResponse &response) const
      {
          s.data->players[s.decision.controllingPlayer].ledger.RecordBuy(response.cards[0]);
          s.stack.PushEnd(new EventTrashCardFromPlay(s.player, c));
          s.stack.PushEnd(new EventGainCard(s.player, response.cards[0]));
      }
  };
  
  class CardChancellor : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          s.decision.MakeDiscreteChoice(c, 2);
          if(decisionText) s.decision.text = "Put your deck into your discard pile?|Yes|No";
      }
      bool CanProcessDecisions() const
      {
          return true;
      }
      void ProcessDecision(State &s, const DecisionResponse &response) const
      {
          if(response.choice == 0)
          {
              if(logging) s.Log("discards their deck");
              PlayerState &p = s.players[s.player];
              p.discard.Append(p.deck);
              p.deck.FreeMemory();
          }
          else
          {
              if(logging) s.Log("does not discard their deck");
          }
      }
  };
  
  class CardMilitia : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          for(const PlayerInfo &p : s.data->players)
          {
              if(p.index != s.player)
              {
                  s.stack.PushEnd(new EventDiscardDownToN(c, p.index, 3));
              }
          }
      }
  };
  
  class CardCouncilRoom : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          for(const PlayerInfo &p : s.data->players)
          {
              if(p.index != s.player)
              {
                  s.stack.PushEnd(new EventDrawCard(p.index));
              }
          }
      }
  };
  
  class CardRemodel : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          if(s.players[s.player].hand.Length() > 0)
          {
              s.decision.SelectCards(c, 1, 1);
              if(decisionText) s.decision.text = "Select a card to trash:";
              s.decision.cardChoices = s.players[s.player].hand;
              s.stack.PushEnd(new EventRemodelExpand(c, 2));
          }
          else
          {
              if(logging) s.Log("has no cards to trash");
          }
      }
  };
  
  class CardMine : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          if(s.players[s.player].TreasureCount() > 0)
          {
              s.decision.SelectCards(c, 1, 1);
              if(decisionText) s.decision.text = "Select a treasure to trash:";
              for(Card *c : s.players[s.player].hand)
              {
                  if(c->isTreasure) s.decision.AddUniqueCard(c);
              }
              s.stack.PushEnd(new EventMine(c));
          }
          else
          {
              if(logging) s.Log("has no treasures to trash");
          }
      }
  };
  
  class CardLibrary : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          if(s.players[s.player].hand.Length() < 7)
          {
              s.stack.PushEnd(new EventLibrary(c));
          }
          else
          {
              if(logging) s.Log("already has 7 cards in hand");
          }
      }
  };
  
  class CardAdventurer : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          s.stack.PushEnd(new EventAdventurer(c));
      }
  };
  
  class CardWitch : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          for(const PlayerInfo &p : s.data->players)
          {
              if(p.index != s.player)
              {
                  s.stack.PushEnd(new EventGainCard(p.index, s.data->baseCards.curse, false, true, GainToDiscard));
              }
          }
      }
  };
  
  class CardGardens : public CardEffect
  {
  public:
      int VictoryPoints(const State &s, UINT playerIndex) const
      {
          return s.players[playerIndex].TotalCards() / 10;
      }
  };
  
  class CardBureaucrat : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          s.stack.PushEnd(new EventGainCard(s.player, s.data->baseCards.silver, false, false, GainToDeckTop));
          for(const PlayerInfo &p : s.data->players)
          {
              if(p.index != s.player)
              {
                  s.stack.PushEnd(new EventBureaucratAttack(c, p.index));
              }
          }
      }
      bool CanProcessDecisions() const
      {
          return true;
      }
      void ProcessDecision(State &s, const DecisionResponse &response) const
      {
          Card *c = response.cards[0];
          PlayerState &p = s.players[s.decision.controllingPlayer];
          p.hand.RemoveSwap(p.hand.FindFirstIndex(c));
          p.deck.PushEnd(c);
          if(logging) s.LogIndent(1, s.decision.controllingPlayer, "puts " + c->PrettyName() + " on their deck");
      }
  };
  
  class CardSpy : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          for(const PlayerInfo &p : s.data->players)
          {
              s.stack.PushEnd(new EventSpy(c, s.player, p.index));
          }
      }
  };
  
  class CardThroneRoom : public CardEffect
  {
  public:
      void PlayAction(State &s) const
      {
          s.stack.PushEnd(new EventPlayActionNTimes(c, 2));
      }
  };
  