// // AttackAnnotations is special annotation that attack events must store. They indicate which reactions // have already been processed for the attack. // struct AttackAnnotations { AttackAnnotations() { moatProcessed = false; } bool moatProcessed; }; // // A list of Event objects composes the event stack of a Dominion game. // If Advance returns true, the event is done and should be removed, otherwise it will keep recurring. // class Event { public: virtual ~Event() {} virtual bool Advance(State &s) = 0; virtual bool IsAttack() const { return false; } virtual int AttackedPlayer() const { return -1; } virtual AttackAnnotations* Annotations() { return NULL; } virtual bool CanProcessDecisions() const { return false; } virtual bool DestroyNextEventOnStack() const { return false; } virtual void ProcessDecision(State &s, const DecisionResponse &response) { SignalError("Event does not support decisions"); } }; // // Making card draw an effect is easy but inefficient since it is only needed for Stash, a rather inelegant card I don't plan to implement. // Still, card draw can be pushed onto the stack by cards such as Cellar. This is because you need to process the discards before drawing, // so that the discarded cards can be drawn. // // ** Possible triggers // Stash // class EventDrawCard : public Event { public: EventDrawCard(UINT _player) { player = _player; } bool Advance(State &s); UINT player; }; class EventTrashCardFromPlay : public Event { public: EventTrashCardFromPlay(UINT _player, Card *_card) { player = _player; card = _card; } bool Advance(State &s); UINT player; Card *card; }; class EventTrashCardFromHand : public Event { public: EventTrashCardFromHand(UINT _player, Card *_card) { player = _player; card = _card; } bool Advance(State &s); UINT player; Card *card; }; class EventTrashCardFromDeck : public Event { public: EventTrashCardFromDeck(UINT _player) { player = _player; } bool Advance(State &s) { Card *c = s.players[player].deck.Last(); if(logging) s.LogIndent(1, player, "trashes " + c->PrettyName() + " from the top of their deck"); s.players[player].deck.PopEnd(); return true; } UINT player; }; class EventTrashCardFromSideZone : public Event { public: EventTrashCardFromSideZone(UINT _player, Card *_c) { player = _player; c = _c; } bool Advance(State &s) { if(logging) s.LogIndent(1, player, "trashes " + c->PrettyName()); return true; } UINT player; Card *c; }; // // When fromSideZone is true, the card is pulled from a special reserved zone instead of the hand. // // ** Possible triggers // Tunnel // class EventDiscardCard : public Event { public: EventDiscardCard(UINT _player, Card *_card, DiscardZone _zone) { player = _player; card = _card; zone = _zone; } bool Advance(State &s); UINT player; Card *card; DiscardZone zone; }; // // ** Possible triggers // Watchtower // Royal seal // Mint // class EventGainCard : public Event { public: EventGainCard(UINT _player, Card *_card) { player = _player; card = _card; bought = false; isAttack = false; zone = GainToDiscard; state = TriggerNone; } EventGainCard(UINT _player, Card *_card, bool _bought, bool _isAttack, GainZone _zone) { player = _player; card = _card; bought = _bought; isAttack = _isAttack; zone = _zone; state = TriggerNone; } bool IsAttack() const { return isAttack; } int AttackedPlayer() const { return player; } AttackAnnotations* Annotations() { return &annotations; } bool CanProcessDecisions() const { return true; } void ProcessDecision(State &s, const DecisionResponse &response) { if(state == TriggerProcessingRoyalSeal) { if(response.choice == 0) { if(logging) s.LogIndent(1, "uses Royal Seal"); zone = GainToDeckTop; } else { if(logging) s.LogIndent(1, "does not use Royal Seal"); } } else if(state == TriggerProcessingWatchtower) { if(response.choice == 1) { if(logging) s.LogIndent(1, player, "reveals " + s.data->baseCards.watchtower->PrettyName()); zone = GainToDeckTop; } else if(response.choice == 0) { if(logging) s.LogIndent(1, player, "reveals " + s.data->baseCards.watchtower->PrettyName()); if(logging) s.LogIndent(1, player, "trashes " + card->PrettyName()); zone = GainToTrash; } else if(response.choice == 2) { if(logging) s.LogIndent(1, player, "doesn't reveal " + s.data->baseCards.watchtower->PrettyName()); } } state = TriggerProcessed; } bool Advance(State &s) { SupplyEntry &curSupply = s.supply[s.data->SupplyIndex(card)]; PlayerState &p = s.players[player]; if(curSupply.count > 0) { if((s.data->watchtowerInSupply || s.data->royalSealInSupply) && state == TriggerNone) { if(p.hand.Contains(s.data->baseCards.watchtower)) { state = TriggerProcessingWatchtower; s.decision.MakeDiscreteChoice(s.data->baseCards.watchtower, 3); s.decision.controllingPlayer = player; if(decisionText) s.decision.text = "About to gain" + card->PrettyName() +":|Trash it|Put it on top of deck|Don't use Watchtower"; return false; } else if(p.CardInPlay(s.data->baseCards.royalSeal)) { state = TriggerProcessingRoyalSeal; s.decision.MakeDiscreteChoice(s.data->baseCards.royalSeal, 2); if(decisionText) s.decision.text = "Put" + card->PrettyName() + "on top of your deck?|Yes|No"; return false; } } if(s.data->useGainList && player == s.player) s.gainList.PushEnd(card); if(s.data->tradeRouteInSupply) { SupplyEntry &entry = s.supply[s.data->SupplyIndex(card)]; if(entry.tradeRouteToken == 1) { entry.tradeRouteToken = 0; s.tradeRouteValue++; } } String zoneModifier; if(zone == GainToHand) { p.hand.PushEnd(card); zoneModifier = " in hand"; } else if(zone == GainToDiscard || zone == GainToDiscardIronworks) { p.discard.PushEnd(card); if(zone == GainToDiscardIronworks) { if(card->isAction) p.actions++; if(card->isTreasure) { if(logging) s.LogIndent(1, player, "gets $1"); p.money++; } if(card->isVictory) s.stack.PushEnd(new EventDrawCard(s.player)); } } else if(zone == GainToDeckTop) { p.deck.PushEnd(card); zoneModifier = " and puts it on their deck"; } curSupply.count--; if(bought) { int cost = s.SupplyCost(card); if(logging) s.Log(player, "buys " + card->PrettyName()); if(logging) s.LogIndent(1, "spends $" + String(cost)); p.money -= cost; p.buys--; } else if(zone != GainToTrash) { if(logging) s.LogIndent(1, player, "gains a " + card->PrettyName() + zoneModifier); } } else { if(bought) { // // We will sometimes get here if the player asks for multiple piles of the same type // if(logging) s.LogIndent(1, player, "cannot buy " + card->PrettyName()); p.buys--; } else { if(logging) s.LogIndent(1, player, "cannot gain " + card->PrettyName()); } } return true; } enum TriggerState { TriggerNone, TriggerProcessed, TriggerProcessingRoyalSeal, TriggerProcessingWatchtower, }; UINT player; Card *card; bool bought; bool isAttack; TriggerState state; GainZone zone; AttackAnnotations annotations; }; class EventDiscardDownToN : public Event { public: EventDiscardDownToN(Card *_source, UINT _player, UINT _handSize) { source = _source; player = _player; handSize = _handSize; done = false; } bool IsAttack() const { return true; } int AttackedPlayer() const { return player; } AttackAnnotations* Annotations() { return &annotations; } bool Advance(State &s) { if(done) return true; int currentHandSize = int(s.players[player].hand.Length()); int cardsToDiscard = currentHandSize - handSize; if(cardsToDiscard <= 0) { if(logging) s.Log(player, "has " + String(currentHandSize) + " cards in hand"); return true; } s.decision.SelectCards(source, cardsToDiscard, cardsToDiscard); s.decision.cardChoices = s.players[player].hand; s.decision.controllingPlayer = player; if(decisionText) { if(cardsToDiscard == 1) s.decision.text = "Choose a card to discard:"; else s.decision.text = "Choose " + String(cardsToDiscard) + " cards to discard:"; } return false; } bool CanProcessDecisions() const { return true; } void ProcessDecision(State &s, const DecisionResponse &response) { for(Card *c : response.cards) { s.stack.PushEnd(new EventDiscardCard(s.decision.controllingPlayer, c, DiscardFromHand)); } done = true; } Card *source; UINT player; UINT handSize; AttackAnnotations annotations; bool done; }; class EventPutOnDeckDownToN : public Event { public: EventPutOnDeckDownToN(Card *_source, UINT _player, UINT _handSize) { source = _source; player = _player; handSize = _handSize; done = false; } bool IsAttack() const { return true; } int AttackedPlayer() const { return player; } AttackAnnotations* Annotations() { return &annotations; } bool Advance(State &s) { if(done) return true; int currentHandSize = int(s.players[player].hand.Length()); int cardsToDiscard = currentHandSize - handSize; if(cardsToDiscard <= 0) { if(logging) s.Log(player, "has " + String(currentHandSize) + " cards in hand"); return true; } s.decision.SelectCards(source, cardsToDiscard, cardsToDiscard); s.decision.cardChoices = s.players[player].hand; s.decision.controllingPlayer = player; if(decisionText) { if(cardsToDiscard == 1) s.decision.text = "Choose a card to put on top of your deck:"; else s.decision.text = "Choose " + String(cardsToDiscard) + " cards to put on top of your deck:"; } return false; } bool CanProcessDecisions() const { return true; } void ProcessDecision(State &s, const DecisionResponse &response) { PlayerState &p = s.players[player]; for(Card *c : response.cards) { if(logging) s.LogIndent(1, player, "puts " + c->PrettyName() + " on top of their deck"); p.hand.RemoveSwap(p.hand.FindFirstIndex(c)); p.deck.PushEnd(c); } done = true; } Card *source; UINT player; UINT handSize; AttackAnnotations annotations; bool done; }; class EventDiscardNCards : public Event { public: EventDiscardNCards(Card *_source, UINT _player, int _minDiscardCount, int _maxDiscardCount) { source = _source; player = _player; minDiscardCount = _minDiscardCount; maxDiscardCount = _maxDiscardCount; done = false; } bool Advance(State &s) { if(done) return true; int currentHandSize = int(s.players[player].hand.Length()); int cardsToDiscard = Math::Min(currentHandSize, maxDiscardCount); minDiscardCount = Math::Min(minDiscardCount, cardsToDiscard); if(cardsToDiscard == 0) { if(logging) s.Log(player, "has no cards to discard"); return true; } s.decision.SelectCards(source, minDiscardCount, cardsToDiscard); s.decision.cardChoices = s.players[player].hand; s.decision.controllingPlayer = player; if(decisionText) { if(cardsToDiscard == 1) s.decision.text = "Choose a card to discard:"; else s.decision.text = "Choose " + String(cardsToDiscard) + " cards to discard:"; } return false; } bool CanProcessDecisions() const { return true; } void ProcessDecision(State &s, const DecisionResponse &response) { for(Card *c : response.cards) { s.stack.PushEnd(new EventDiscardCard(s.decision.controllingPlayer, c, DiscardFromHand)); } if(source == s.data->baseCards.furnace && response.cards.Length() > 0) { if(logging) s.LogIndent(1, "gets $1 from " + source->PrettyName()); s.players[s.player].money++; } done = true; } Card *source; UINT player; int minDiscardCount, maxDiscardCount; bool done; }; class EventPlayActionNTimes : public Event { public: EventPlayActionNTimes(Card *_source, UINT _count) { source = _source; target = NULL; count = _count; } bool CanProcessDecisions() const { return true; } bool Advance(State &s) { if(count == 0) return true; PlayerState &p = s.players[s.player]; if(target == NULL) { if(p.ActionCount() == 0) { if(logging) s.Log("has no actions to play"); return true; } s.decision.SelectCards(source, 1, 1); if(decisionText) s.decision.text = "Select an action to play " + String(count) + " times:"; if(source == s.data->baseCards.trailblazer) { for(Card *c : p.hand) { if(c->isAction && s.SupplyCost(c) <= 3) s.decision.AddUniqueCard(c); } if(s.decision.cardChoices.Length() == 0) { s.decision.type = DecisionNone; if(logging) s.Log("has no actions to play"); return true; } } else { for(Card *c : p.hand) { if(c->isAction) s.decision.AddUniqueCard(c); } } } else { if(logging) s.Log("plays " + target->PrettyName()); count--; s.ProcessAction(target); } return false; } void ProcessDecision(State &s, const DecisionResponse &response) { PlayerState &p = s.players[s.player]; target = response.cards[0]; int index = p.hand.FindFirstIndex(target); p.hand.RemoveSwap(index); if(source == s.data->baseCards.witchdoctor) { int supplyCount = s.SupplyCount(target); if(supplyCount == 0) { if(logging) s.LogIndent(1, "cannot gain " + target->PrettyName()); count = 1; } else { s.stack.PushEnd(new EventGainCard(s.player, target, false, false, GainToDiscard)); } } CardPlayInfo newInfo(target, 1); newInfo.copies = count; p.playArea.PushEnd(newInfo); } UINT count; Card *source, *target; }; class EventChooseCardsToTrash : public Event { public: EventChooseCardsToTrash(Card *_source, int _minCards, int _maxCards) { done = false; source = _source; minCards = _minCards; maxCards = _maxCards; } bool CanProcessDecisions() const { return true; } bool Advance(State &s) { if(done) return true; minCards = Math::Min(minCards, (int)s.players[s.player].hand.Length()); maxCards = Math::Min(maxCards, (int)s.players[s.player].hand.Length()); if(maxCards == 0) { if(logging) s.LogIndent(1, "has no cards to trash"); s.decision.type = DecisionNone; return true; } s.decision.SelectCards(source, minCards, maxCards); if(decisionText) { if(minCards == maxCards && minCards == 1) s.decision.text = "Choose a card to trash:"; else if(minCards == maxCards) s.decision.text = "Choose " + String(minCards) + " cards to trash:"; else if(minCards == 0) s.decision.text = "Choose up to " + String(maxCards) + " cards to trash:"; else s.decision.text = "Choose cards to trash:"; } s.decision.cardChoices = s.players[s.player].hand; return false; } void ProcessDecision(State &s, const DecisionResponse &response) { PlayerState &p = s.players[s.player]; for(Card *c : response.cards) { s.stack.PushEnd(new EventTrashCardFromHand(s.player, c)); } if(source == s.data->baseCards.evangelist && response.cards.Length() == 0) { s.stack.PushEnd(new EventTrashCardFromPlay(s.player, s.data->baseCards.evangelist)); } done = true; } Card *source; int minCards; int maxCards; bool done; }; class EventReorderDeck : public Event { public: EventReorderDeck(Card *_source, UINT _player, int _cardCount) { done = false; source = _source; player = _player; cardCount = _cardCount; } bool CanProcessDecisions() const { return true; } bool Advance(State &s) { if(done) return true; PlayerState &p = s.players[player]; s.decision.SelectCards(source, 1, 1); s.decision.controllingPlayer = player; if(decisionText) { if(cardCount == 2) s.decision.text = "Choose a card to put 2nd from the top:"; else if(cardCount == 3) s.decision.text = "Choose a card to put 3rd from the top:"; else if(cardCount == 4) s.decision.text = "Choose a card to put 4th from the top:"; else if(cardCount == 5) s.decision.text = "Choose a card to put 5th from the top:"; else s.decision.text = "Choose a card to on your deck:"; } for(int cardIndex = 0; cardIndex < cardCount; cardIndex++) { s.decision.AddUniqueCard(p.deck[p.deck.Length() - 1 - cardIndex]); } return false; } void ProcessDecision(State &s, const DecisionResponse &response) { if(logging) s.LogIndent(1, player, "puts " + response.cards[0]->PrettyName() + " on top of their deck"); PlayerState &p = s.players[player]; for(int cardIndex = 0; cardIndex < cardCount; cardIndex++) { if(p.deck[p.deck.Length() - 1 - cardIndex] == response.cards[0]) { Utility::Swap(p.deck[p.deck.Length() - cardCount], p.deck[p.deck.Length() - 1 - cardIndex]); if(logging && cardCount == 2) s.LogIndent(1, player, "puts " + p.deck.Last()->PrettyName() + " on top of their deck"); done = true; return; } } SignalError("Response card not found"); } Card *source; UINT player; int cardCount; bool done; }; class EventChooseCardToGain : public Event { public: EventChooseCardToGain(Card *_source, UINT _controllingPlayer, UINT _gainingPlayer, bool _optionalGain, int _minCost, int _maxCost, CardFilter _filter, GainZone _zone) { source = _source; controllingPlayer = _controllingPlayer; gainingPlayer = _gainingPlayer; minCost = _minCost; maxCost = _maxCost; optionalGain = _optionalGain; zone = _zone; filter = _filter; done = false; } bool CanProcessDecisions() const { return true; } bool Advance(State &s) { if(done) return true; s.decision.GainCardFromSupply(s, source, minCost, maxCost, filter); if(optionalGain) s.decision.minimumCards = 0; s.decision.controllingPlayer = controllingPlayer; if(s.decision.cardChoices.Length() == 0) { if(logging) s.LogIndent(1, gainingPlayer, "has no cards to gain"); s.decision.type = DecisionNone; return true; } return false; } void ProcessDecision(State &s, const DecisionResponse &response) { if(response.cards.Length() > 0) { s.stack.PushEnd(new EventGainCard(gainingPlayer, response.cards[0], false, false, zone)); } else { if(logging) s.LogIndent(1, gainingPlayer, "does not gain a card"); } done = true; } Card *source; UINT controllingPlayer, gainingPlayer; int minCost, maxCost; GainZone zone; CardFilter filter; bool optionalGain; bool done; };