using DominionBase.Currencies; using DominionBase.Enums; using DominionBase.Piles; using DominionBase.Players; using DominionBase.Properties; using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; namespace DominionBase.Cards.Prosperity { public static class TypeClass { public static readonly Type Bank = typeof(Bank); public static readonly Type Bishop = typeof(Bishop); public static readonly Type City = typeof(City); public static readonly Type Colony = typeof(Colony); public static readonly Type Contraband = typeof(Contraband); public static readonly Type CountingHouse = typeof(CountingHouse); public static readonly Type Expand = typeof(Expand); public static readonly Type Forge = typeof(Forge); public static readonly Type Goons = typeof(Goons); public static readonly Type GrandMarket = typeof(GrandMarket); public static readonly Type Hoard = typeof(Hoard); public static readonly Type KingsCourt = typeof(KingsCourt); public static readonly Type Loan = typeof(Loan); public static readonly Type Mint = typeof(Mint); public static readonly Type Monument = typeof(Monument); public static readonly Type Mountebank = typeof(Mountebank); public static readonly Type Peddler = typeof(Peddler); public static readonly Type Platinum = typeof(Platinum); public static readonly Type Quarry = typeof(Quarry); public static readonly Type Rabble = typeof(Rabble); public static readonly Type RoyalSeal = typeof(RoyalSeal); public static readonly Type Talisman = typeof(Talisman); public static readonly Type TradeRoute = typeof(TradeRoute); public static readonly Type Vault = typeof(Vault); public static readonly Type Venture = typeof(Venture); public static readonly Type Watchtower = typeof(Watchtower); public static readonly Type WorkersVillage = typeof(WorkersVillage); public static readonly Type ContrabandToken = typeof(ContrabandMarker); public static readonly Type TradeRouteToken = typeof(TradeRouteToken); public static readonly Type VictoryToken = typeof(VictoryToken); } public class Bank : Card { public Bank() : base(Categories.Treasure, Source.Prosperity, Location.Kingdom, Traits.PlusCoin | Traits.ConditionalBenefit) { BaseCost = new Cost(7); Benefit.Currency.Coin.IsVariable = true; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var benefit = new CardBenefit(); benefit.Currency += new Coin(player.InPlay[Categories.Treasure].Count); player.ReceiveBenefit(this, benefit); } } public class Bishop : Card { public Bishop() : this(Edition.First) { } public Bishop(Edition edition) : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.Component | Traits.DeckReduction | Traits.PlusCoin | Traits.Trasher | Traits.RemoveCurses | Traits.AffectOthers | Traits.Terminal | Traits.ConditionalBenefit | Traits.VPTokens | Traits.TrashForBenefit | Traits.RemoveFromHand, edition: edition) { BaseCost = new Cost(4); Benefit.Currency.Coin.Value = 1; Benefit.VictoryPoints = 1; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var choice = new Choice("Trash a card. + equal to half its cost in coins, rounded down.", this, player.Hand, ChoiceOutcome.Trash, player); var result = player.MakeChoice(choice); if (result.Cards.Any()) { var trash = player.RetrieveCardFrom(DeckLocation.Hand, result.Cards[0]); var trashedCardCost = player._Game.ComputeCost(trash); player.Trash(this, trash); player.ReceiveBenefit(this, new CardBenefit { VictoryPoints = trashedCardCost.Coin.Value / 2 }); } var enumerator = player._Game.GetPlayersStartingWithEnumerator(player); enumerator.MoveNext(); // skip active player while (enumerator.MoveNext()) { var otherPlayer = enumerator.Current; var choicePlayer = new Choice("Trash a card if you wish", this, otherPlayer.Hand, ChoiceOutcome.Trash, otherPlayer, minimum: 0); var resultPlayer = otherPlayer.MakeChoice(choicePlayer); if (resultPlayer.Cards.Any()) { otherPlayer.Trash(this, otherPlayer.RetrieveCardFrom(DeckLocation.Hand, resultPlayer.Cards[0])); } } } } public class City : Card { public City() : this(Edition.First) { } public City(Edition edition) : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.PlusCard | Traits.PlusAction | Traits.PlusMultipleActions | Traits.PlusCoin | Traits.PlusBuy | Traits.ConditionalBenefit | Traits.Cantrip, edition: edition) { BaseCost = new Cost(5); Benefit.Actions = 2; Benefit.Cards = 1; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var emptyPiles = player._Game.Table.EmptySupplyPiles; if (emptyPiles > 0) { player.ReceiveBenefit(this, new CardBenefit { Cards = 1 }); if (emptyPiles > 1) player.ReceiveBenefit(this, new CardBenefit { Currency = new Currency(1), Buys = 1 }); } } } public class Colony : Card { public Colony() : base(Categories.Victory, Source.Prosperity, Location.General) { BaseCost = new Cost(11); VictoryPoints = 10; } public override bool IsEndgameTriggered(ISupply supply) { Contract.Requires(supply != null, "supply cannot be null"); return (supply.Count == 0); } } public class Contraband : Card { private IPlayer _turnEndedPlayer; private TurnEndedEventHandler _turnEndedEventHandler; private readonly Dictionary _buyCheckEventHandlers = new Dictionary(); public Contraband() : base(Categories.Treasure, Source.Prosperity, Location.Kingdom, Traits.PlusBuy | Traits.PlusCoin) { BaseCost = new Cost(5); Benefit.Buys = 1; Benefit.Currency.Coin.Value = 3; } public override void TearDown(IGame game) { base.TearDown(game); foreach (var supply in _buyCheckEventHandlers.Keys.OfType()) supply.BuyCheck -= _buyCheckEventHandlers[supply]; _buyCheckEventHandlers.Clear(); if (_turnEndedEventHandler != null && _turnEndedPlayer != null) Player_TurnEnded(_turnEndedPlayer, new TurnEndedEventArgs(_turnEndedPlayer)); _turnEndedEventHandler = null; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); if (_turnEndedEventHandler == null) { _turnEndedPlayer = player; _turnEndedEventHandler = new TurnEndedEventHandler(Player_TurnEnded); player.TurnEnded += _turnEndedEventHandler; } // Get the player to my left var playerToLeft = player._Game.GetPlayerFromIndex(player, 1); var availableSupplies = new SupplyCollection(player._Game.Table.TableEntities.FindAll( kvp => (kvp.Randomizer != null && !kvp.Randomizer.Traits.HasFlag(Traits.Randomizer)) || kvp.Location == Location.General )); var cards = new Dictionary(); var choice = new Choice($"Name a card {player} can't buy", this, availableSupplies, ChoiceOutcome.Select, player, false); foreach (var supply in player._Game.Table.TableEntities.Values.OfType()) { foreach (var type in supply.Types) { if (choice.Supplies.All(kvp => kvp.Value is ISupply kvpSupply && kvpSupply.Type != type)) { if (!cards.ContainsKey(supply)) cards[supply] = new CardCollection(); cards[supply].Add(CreateInstance(type)); } } } choice.AddCards(cards.SelectMany(s => s.Value).OrderBy(c => c.Name)); var result = playerToLeft.MakeChoice(choice); IDisplayable namedCard = result.Supply; var namedSupply = result.Supply; if (namedSupply == null) { namedCard = result.Cards[0]; namedSupply = cards.First(s => s.Value.Contains(namedCard)).Key; } player._Game.SendMessage(playerToLeft, this, namedCard); if (!_buyCheckEventHandlers.ContainsKey(namedCard)) { namedSupply.AddToken(new ContrabandMarker(namedCard)); _buyCheckEventHandlers[namedCard] = new BuyCheckEventHandler(Supply_BuyCheck); namedSupply.BuyCheck += _buyCheckEventHandlers[namedCard]; } } private void Supply_BuyCheck(object sender, BuyCheckEventArgs e) { // Already been cancelled -- don't need to process this one if (e.Cancelled) return; var supply = (ISupply)sender; var contrabandTokens = supply.Tokens.OfType(); if (contrabandTokens.Any(c => c.Unbuyable.Name == supply.TopCard.Name)) e.Cancelled = true; } private void Player_TurnEnded(object sender, TurnEndedEventArgs e) { var player = sender as IPlayer; if (_turnEndedEventHandler != null && _turnEndedPlayer != null) _turnEndedPlayer.TurnEnded -= _turnEndedEventHandler; _turnEndedPlayer = null; _turnEndedEventHandler = null; foreach (var namedCard in _buyCheckEventHandlers.Keys) { var supply = namedCard as ISupply ?? player._Game.Table.TableEntities.Values.OfType().First(s => s.Types.Contains(namedCard.Type)); supply.BuyCheck -= _buyCheckEventHandlers[namedCard]; } _buyCheckEventHandlers.Clear(); } } public class ContrabandMarker : Token { public IDisplayable Unbuyable { get; } public ContrabandMarker() : base("X", "Unbuyable") { } public ContrabandMarker(IDisplayable unbuyable) : this() { Unbuyable = unbuyable; } public override string Title => $"{Unbuyable.Name} cannot be bought this turn"; public override bool IsTemporary => true; } public class CountingHouse : Card { public CountingHouse() : this(Edition.First) { } public CountingHouse(Edition edition) : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.PlusCard | Traits.Terminal, edition: edition) { BaseCost = new Cost(5); } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var discardPileCards = player.DiscardPile.LookThrough(c => true); player._Game.SendMessage(player, this, "LookThroughDiscard", discardPileCards.ToArray()); var copperCount = discardPileCards.Count(c => c is Universal.Copper); var options = new List(); for (var i = 0; i <= copperCount; i++) options.Add(i.ToString(CultureInfo.InvariantCulture)); var choice = new Choice("How many Copper cards would you like to reveal and put into your hand?", this, this, options, player); var result = player.MakeChoice(choice); var number = int.Parse(result.Options[0], CultureInfo.InvariantCulture); player.AddCardsInto(DeckLocation.Revealed, player.RetrieveCardsFrom(DeckLocation.Discard, Universal.TypeClass.Copper, number)); player.AddCardsToHand(DeckLocation.Revealed); } } public class Expand : Card { public Expand() : this(Edition.First) { } public Expand(Edition edition) : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.Gainer | Traits.Trasher | Traits.RemoveCurses | Traits.Terminal | Traits.TrashForBenefit | Traits.RemoveFromHand, edition: edition) { BaseCost = new Cost(7); } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var choiceTrash = new Choice(Resource.ChooseACardToTrash, this, player.Hand, ChoiceOutcome.Trash, player); var resultTrash = player.MakeChoice(choiceTrash); player.Trash(this, player.RetrieveCardsFrom(DeckLocation.Hand, resultTrash.Cards)); if (resultTrash.Cards.Any()) { var trashedCardCost = player._Game.ComputeCost(resultTrash.Cards[0]); var gainableSupplies = new SupplyCollection(player._Game.Table.TableEntities.FindAll(supply => supply.CanGain() && supply.CurrentCost <= trashedCardCost + new Coin(3))); var choice = new Choice(Resource.GainCard, this, gainableSupplies, ChoiceOutcome.Gain, player, false); var result = player.MakeChoice(choice); if (result.Supply != null) player.Gain(result.Supply, this); } } } public class Forge : Card { public Forge() : this(Edition.First) { } public Forge(Edition edition) : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.DeckReduction | Traits.Gainer | Traits.Trasher | Traits.RemoveCurses | Traits.Terminal | Traits.TrashForBenefit | Traits.RemoveFromHand, edition: edition) { BaseCost = new Cost(7); } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var choiceTrash = new Choice("Choose any number of cards to trash", this, player.Hand, ChoiceOutcome.Trash, player, minimum: 0, maximum: player.Hand.Count); var resultTrash = player.MakeChoice(choiceTrash); player.Trash(this, player.RetrieveCardsFrom(DeckLocation.Hand, resultTrash.Cards)); var totalCoinCost = new Coin(); foreach (var card in resultTrash.Cards) totalCoinCost += player._Game.ComputeCost(card).Coin; var gainableSupplies = new SupplyCollection(player._Game.Table.TableEntities.FindAll(supply => supply.CanGain() && supply.CurrentCost == new Cost(totalCoinCost))); var choice = new Choice(Resource.GainCard, this, gainableSupplies, ChoiceOutcome.Gain, player, false); var result = player.MakeChoice(choice); if (result.Supply != null) player.Gain(result.Supply, this); } } public class Goons : Card { private CardBoughtEventHandler _cardBoughtHandler; public Goons() : this(Edition.First) { } public Goons(Edition edition) : base(Categories.Action | Categories.Attack, Source.Prosperity, Location.Kingdom, Traits.Component | Traits.PlusCoin | Traits.PlusBuy | Traits.Discard | Traits.Terminal | Traits.VPTokens | Traits.ReactToBuy, edition: edition) { BaseCost = new Cost(6); Benefit.Buys = 1; Benefit.Currency.Coin.Value = 2; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var enumerator = player._Game.GetPlayersStartingWithEnumerator(player); enumerator.MoveNext(); while (enumerator.MoveNext()) { var attackee = enumerator.Current; // Skip if the attack is blocked (Moat, Lighthouse, etc.) if (IsAttackBlocked[attackee]) continue; var choice = new Choice(Resource.DiscardDownTo3Cards, this, attackee.Hand, ChoiceOutcome.Discard, attackee, minimum: attackee.Hand.Count - 3, maximum: attackee.Hand.Count - 3); var result = attackee.MakeChoice(choice); attackee.Discard(DeckLocation.Hand, result.Cards); } } public override void AddedTo(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.AddedTo(location, player); if (location == DeckLocation.InPlay) { if (_cardBoughtHandler != null) player.CardBought -= _cardBoughtHandler; _cardBoughtHandler = new CardBoughtEventHandler(Player_CardBought); player.CardBought += _cardBoughtHandler; } } private void Player_CardBought(object sender, CardBuyEventArgs e) { // Already been cancelled or processed -- don't need to process this one if (e.Cancelled || e.HandledBy.Contains(this) || !e.Card.Type.IsSubclassOf(typeof(Card))) return; e.HandledBy.Add(this); (sender as IPlayer).ReceiveBenefit(this, new CardBenefit { VictoryPoints = 1 }); } public override void RemovedFrom(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.RemovedFrom(location, player); if (_cardBoughtHandler != null) player.CardBought -= _cardBoughtHandler; _cardBoughtHandler = null; } } public class GrandMarket : Card { public GrandMarket() : this(Edition.First) { } public GrandMarket(Edition edition) : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.PlusCard | Traits.PlusAction | Traits.PlusCoin | Traits.PlusBuy | Traits.Cantrip, edition: edition) { BaseCost = new Cost(6); Benefit.Actions = 1; Benefit.Cards = 1; Benefit.Buys = 1; Benefit.Currency.Coin.Value = 2; } public override bool CanBuy(IPlayer player, Currency currency) { Contract.Requires(player != default(IPlayer), "player cannot be null"); if (player.InPlay[Universal.TypeClass.Copper].Count > 0) return false; return base.CanBuy(player, currency); } } public class Hoard : Card { private CardBoughtEventHandler _cardBoughtHandler; public Hoard() : base(Categories.Treasure, Source.Prosperity, Location.Kingdom, Traits.Gainer | Traits.PlusCoin | Traits.ReactToBuy) { BaseCost = new Cost(6); Benefit.Currency.Coin.Value = 2; } protected override bool AllowUndo => true; public override void AddedTo(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.AddedTo(location, player); if (location == DeckLocation.InPlay) { if (_cardBoughtHandler != null) player.CardBought -= _cardBoughtHandler; _cardBoughtHandler = new CardBoughtEventHandler(Player_CardBought); player.CardBought += _cardBoughtHandler; } } private void Player_CardBought(object sender, CardBuyEventArgs e) { if (e.HandledBy.Contains(this) || e.Resolvers.ContainsKey(Type) || !e.Card.Category.HasFlag(Categories.Victory) || !e.Game.Table.Gold.CanGain() || !e.Card.Type.IsSubclassOf(typeof(Card))) return; e.Resolvers[Type] = new CardBuyResolver(Owner, this, "Gain a Gold", Player_BuyWithHoard, true); } internal void Player_BuyWithHoard(IPlayer player, ref CardBuyEventArgs e) { player.Gain(e.Game.Table.Gold, this); e.HandledBy.Add(this); } public override void RemovedFrom(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.RemovedFrom(location, player); if (_cardBoughtHandler != null) player.CardBought -= _cardBoughtHandler; _cardBoughtHandler = null; } } public class KingsCourt : Card { private readonly List CardsPlayed = new List(); public KingsCourt() : this(Edition.First) { } public KingsCourt(Edition edition) : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.Multiplier, edition: edition) { BaseCost = new Cost(7); } public override void AddedTo(DeckLocation location, IPlayer player) { base.AddedTo(location, player); CardsPlayed.Clear(); } public override bool CanCleanUp { // If King's Court played a Duration card but the Duration card only did something once (e.g. Tactician), then King's Court can be cleaned up get => CardsPlayed.All(cp => cp.CanCleanUpPlayed.Count(cup => !cup) < 2) && base.CanCleanUp; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var choice = new Choice(Resource.ChooseActionPlay3x, this, player.Hand[Categories.Action], ChoiceOutcome.Select, player, minimum: 0); var result = player.MakeChoice(choice); if (result.Cards.Any()) { var cardToPlayThrice = result.Cards[0]; CardsPlayed.Add(cardToPlayThrice); cardToPlayThrice.ModifiedBy = this; player.Actions++; var previousPlayerMode = player.PutCardIntoPlay(cardToPlayThrice); var logicalCard = cardToPlayThrice.LogicalCard; player.PlayCard(cardToPlayThrice.LogicalCard, previousPlayerMode); player.Actions++; previousPlayerMode = player.PutCardIntoPlay(cardToPlayThrice, Resource.AgainFromCard.Replace("{card}", this.ToString())); player.PlayCard(logicalCard, previousPlayerMode); player.Actions++; previousPlayerMode = player.PutCardIntoPlay(cardToPlayThrice, Resource.AgainFromCard.Replace("{card}", this.ToString())); player.PlayCard(logicalCard, previousPlayerMode); } else player.PlayNothing(); } protected override void ModifyDuration(IPlayer player, Card card) { base.ModifyDuration(player, card); base.ModifyDuration(player, card); base.ModifyDuration(player, card); } } public class Loan : Card { public Loan() : this(Edition.First) { } public Loan(Edition edition) : base(Categories.Treasure, Source.Prosperity, Location.Kingdom, Traits.DeckReduction | Traits.Trasher | Traits.PlusCoin | Traits.Discard, edition: edition) { BaseCost = new Cost(3); Benefit.Currency.Coin.Value = 1; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); player.BeginDrawing(); while (player.Revealed[Categories.Treasure].Count < 1 && player.CanDraw) player.Draw(DeckLocation.Revealed); player.EndDrawing(); var treasureCards = player.Revealed[c => c.Category.HasFlag(Categories.Treasure)]; if (treasureCards.Any()) { var card = treasureCards[0]; var discardText = Resource.DiscardCard.Replace("{card}", card.ToString()); var trashText = Resource.TrashCard.Replace("{card}", card.ToString()); var choice = new Choice($"You revealed a {card.Name}.", this, card, new List { discardText, trashText }, player); var result = player.MakeChoice(choice)?.Options.FirstOrDefault(); if (result == discardText) player.Discard(DeckLocation.Revealed, card); else if (result == trashText) player.Trash(this, player.RetrieveCardFrom(DeckLocation.Revealed, card)); } player.DiscardRevealed(); } } public class Mint : Card { private readonly Dictionary _cardBoughtHandlers = new Dictionary(); public Mint() : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.DeckReduction | Traits.Gainer | Traits.Trasher | Traits.Terminal | Traits.ReactToBuy) { BaseCost = new Cost(5); } public override void TearDown(IGame game) { base.TearDown(game); foreach (var playerLoop in _cardBoughtHandlers.Keys) playerLoop.CardBought -= _cardBoughtHandlers[playerLoop]; _cardBoughtHandlers.Clear(); } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); if (player.Hand[Categories.Treasure].Any()) { var choice = new Choice("You may reveal a Treasure card to gain a copy of it.", this, player.Hand[Categories.Treasure], ChoiceOutcome.Gain, player, minimum: 0); var result = player.MakeChoice(choice); if (result.Cards.Any()) { player.AddCardInto(DeckLocation.Revealed, player.RetrieveCardFrom(DeckLocation.Hand, result.Cards[0])); player.AddCardInto(DeckLocation.Hand, player.RetrieveCardFrom(DeckLocation.Revealed, result.Cards[0])); var supply = player._Game.Table.FindSupplyPileByCard(result.Cards[0]); if (supply?.TopCard != null && supply.TopCard.Name == result.Cards[0].Name) player.Gain(supply, this); } } } public override void AddedToSupply(IGame game, ISupply supply) { Contract.Requires(game != null, "game cannot be null"); base.AddedToSupply(game, supply); ResetTriggers(game); } private void ResetTriggers(IGame game) { var enumPlayers = game.GetPlayersStartingWithActiveEnumerator(); while (enumPlayers.MoveNext()) { _cardBoughtHandlers[enumPlayers.Current] = new CardBoughtEventHandler(Player_CardBought); enumPlayers.Current.CardBought += _cardBoughtHandlers[enumPlayers.Current]; } } private void Player_CardBought(object sender, CardBuyEventArgs e) { // This is not the card you are looking for if (e.Card != this || e.Resolvers.ContainsKey(Type) || !e.Card.Type.IsSubclassOf(typeof(Card))) return; e.Resolvers[Type] = new CardBuyResolver(Owner, this, "Trash all Treasures in play", Player_BuyMint, true); } internal void Player_BuyMint(IPlayer player, ref CardBuyEventArgs e) { player.Trash(this, player.RetrieveCardsFrom(DeckLocation.InPlay, Categories.Treasure)); player.Trash(this, player.RetrieveCardsFrom(DeckLocation.SetAside, Categories.Treasure)); e.HandledBy.Add(Type); // Clear out the Event Triggers -- this only happens when its Gained, so we don't care any more foreach (var playerLoop in _cardBoughtHandlers.Keys) playerLoop.CardBought -= _cardBoughtHandlers[playerLoop]; _cardBoughtHandlers.Clear(); } } public class Monument : Card { public Monument() : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.Component | Traits.PlusCoin | Traits.Terminal | Traits.VPTokens) { BaseCost = new Cost(4); Benefit.Currency.Coin.Value = 2; Benefit.VictoryPoints = 1; } protected override bool AllowUndo => true; } public class Mountebank : Card { public Mountebank() : this(Edition.First) { } public Mountebank(Edition edition) : base(Categories.Action | Categories.Attack, Source.Prosperity, Location.Kingdom, Traits.PlusCurses | Traits.PlusCoin | Traits.Discard | Traits.Terminal, edition: edition) { BaseCost = new Cost(5); Benefit.Currency.Coin.Value = 2; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); // Perform attack on every other player var enumerator = player._Game.GetPlayersStartingWithEnumerator(player); enumerator.MoveNext(); while (enumerator.MoveNext()) { var attackee = enumerator.Current; // Skip if the attack is blocked (Moat, Lighthouse, etc.) if (IsAttackBlocked[attackee]) continue; var discardedCurse = false; if (attackee.Hand[Categories.Curse].Any()) { var choice = new Choice("Do you want to discard a Curse or gain a Curse and a Copper?", this, this, new List() { "Discard Curse", "Gain Curse & Copper" }, attackee); var result = attackee.MakeChoice(choice); if (result.Options[0] == "Discard Curse") { discardedCurse = true; attackee.Discard(DeckLocation.Hand, Universal.TypeClass.Curse, 1); } } if (!discardedCurse) { attackee.Gain(player._Game.Table.Curse, this); attackee.Gain(player._Game.Table.Copper, this); } } } } public class Peddler : Card { public Peddler() : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.PlusCard | Traits.PlusAction | Traits.PlusCoin | Traits.ModifyCost | Traits.VariableCost | Traits.Cantrip) { BaseCost = new Cost(8, special: true); Benefit.Cards = 1; Benefit.Actions = 1; Benefit.Currency.Coin.Value = 1; } public override void SetupCard(IGame game) { Contract.Requires(game != null, "game cannot be null"); base.SetupCard(game); game.CostCompute += Peddler_CostCompute; } public override void TearDown(IGame game) { Contract.Requires(game != null, "game cannot be null"); base.TearDown(game); game.CostCompute -= Peddler_CostCompute; } private void Peddler_CostCompute(object sender, CostComputeEventArgs e) { if (e.Card != this) return; var game = sender as IGame; if (game.ActivePlayer != null && (game.ActivePlayer.Phase == PhaseEnum.Buy || game.ActivePlayer.Phase == PhaseEnum.BuyTreasure)) { var actionCardsInPlay = game.ActivePlayer.InPlay[Categories.Action].Count + game.ActivePlayer.SetAside[Categories.Action].Count; e.Cost.Coin -= 2 * actionCardsInPlay; } } } public class Platinum : Card { public Platinum() : base(Categories.Treasure, Source.Prosperity, Location.General) { BaseCost = new Cost(9); Benefit.Currency.Coin.Value = 5; } protected override bool AllowUndo => true; } public class Quarry : Card { private CostComputeEventHandler _costComputeEventHandler; public Quarry() : base(Categories.Treasure, Source.Prosperity, Location.Kingdom, Traits.ModifyCost | Traits.PlusCoin) { BaseCost = new Cost(4); Benefit.Currency.Coin.Value = 1; } protected override bool AllowUndo => true; public override void AddedTo(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.AddedTo(location, player); if (location == DeckLocation.InPlay) { if (_costComputeEventHandler != null) player._Game.CostCompute -= _costComputeEventHandler; _costComputeEventHandler = new CostComputeEventHandler(Player_QuarryInPlayArea); player._Game.CostCompute += _costComputeEventHandler; player._Game.SendMessage(player, this, 2); } } private void Player_QuarryInPlayArea(object sender, CostComputeEventArgs e) { if (e.Card.Category.HasFlag(Categories.Action)) e.Cost.Coin -= 2; } public override void RemovedFrom(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.RemovedFrom(location, player); if (_costComputeEventHandler != null) player._Game.CostCompute -= _costComputeEventHandler; _costComputeEventHandler = null; } } public class Rabble : Card { public Rabble() : this(Edition.First) { } public Rabble(Edition edition) : base(Categories.Action | Categories.Attack, Source.Prosperity, Location.Kingdom, Traits.CardOrdering | Traits.PlusCard | Traits.Discard | Traits.Terminal | Traits.NetCardDraw, edition: edition) { BaseCost = new Cost(5); Benefit.Cards = 3; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); // Perform attack on every other player var enumerator = player._Game.GetPlayersStartingWithEnumerator(player); enumerator.MoveNext(); while (enumerator.MoveNext()) { var attackee = enumerator.Current; // Skip if the attack is blocked (Moat, Lighthouse, etc.) if (IsAttackBlocked[attackee]) continue; attackee.Draw(3, DeckLocation.Revealed); Predicate actionsTreasures = (c => c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure)); attackee.Discard(DeckLocation.Revealed, actionsTreasures); var replaceChoice = new Choice(Resource.ChooseOrderToTopdeck, this, attackee.Revealed, ChoiceOutcome.Select, player, isOrdered: true, minimum: attackee.Revealed.Count, maximum: attackee.Revealed.Count); var replaceResult = attackee.MakeChoice(replaceChoice); attackee.AddCardsToDeck(attackee.RetrieveCardsFrom(DeckLocation.Revealed, replaceResult.Cards), DeckPosition.Top); } } } public class RoyalSeal : Card { private CardGainedEventHandler _cardGainedHandler; public RoyalSeal() : this(Edition.First) { } public RoyalSeal(Edition edition) : base(Categories.Treasure, Source.Prosperity, Location.Kingdom, Traits.CardOrdering | Traits.PlusCoin, edition: edition) { BaseCost = new Cost(5); Benefit.Currency.Coin.Value = 2; } protected override bool AllowUndo => true; public override void AddedTo(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.AddedTo(location, player); if (location == DeckLocation.InPlay) { if (_cardGainedHandler != null) player.CardGained -= _cardGainedHandler; _cardGainedHandler = new CardGainedEventHandler(Player_CardGained); player.CardGained += _cardGainedHandler; } } private void Player_CardGained(object sender, Players.CardGainEventArgs e) { var player = sender as IPlayer; var key = Type.ToString(); // Already been cancelled -- don't need to process this one if (e.Cancelled || e.Resolvers.ContainsKey(key)) return; e.Resolvers[key] = new CardGainResolver(player, this, "PutOnDeck", Resource.PutOnDeck, Player_Action, false); } internal void Player_Action(IPlayer player, ref Players.CardGainEventArgs e) { e.Cancelled = true; e.Location = DeckLocation.Deck; e.Position = DeckPosition.Top; e.HandledBy.Add(Type); } public override void RemovedFrom(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.RemovedFrom(location, player); if (_cardGainedHandler != null) player.CardGained -= _cardGainedHandler; _cardGainedHandler = null; } } public class Talisman : Card { private CardBoughtEventHandler _CardBoughtHandler; public Talisman() : this(Edition.First) { } public Talisman(Edition edition) : base(Categories.Treasure, Source.Prosperity, Location.Kingdom, Traits.Gainer | Traits.PlusCoin | Traits.ReactToBuy, edition: edition) { BaseCost = new Cost(4); Benefit.Currency.Coin.Value = 1; } protected override bool AllowUndo => true; public override void AddedTo(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.AddedTo(location, player); if (location == DeckLocation.InPlay) { if (_CardBoughtHandler != null) player.CardBought -= _CardBoughtHandler; _CardBoughtHandler = new CardBoughtEventHandler(Player_CardBought); player.CardBought += _CardBoughtHandler; } } private void Player_CardBought(object sender, CardBuyEventArgs e) { // Already been cancelled -- don't need to process this one if (e.Cancelled) return; if (e.HandledBy.Contains(this) || e.Resolvers.ContainsKey(Type) || !e.Card.Type.IsSubclassOf(typeof(Card))) return; if (!e.Card.Category.HasFlag(Categories.Victory) && ((ISupply)e.Game.Table[e.Card]).CurrentCost <= new Coin(4)) { e.Resolvers[Type] = new CardBuyResolver(Owner, this, $"Gain a copy of {e.Card}", Player_GainFromTalisman, true); } } internal void Player_GainFromTalisman(IPlayer player, ref CardBuyEventArgs e) { var supply = (ISupply)e.Game.Table[e.Card.Type]; if (supply != null && supply.CanGain() && supply.TopCard.Name == e.Card.Name) player.Gain((ISupply)e.Game.Table[e.Card.Type], this); e.HandledBy.Add(this); } public override void RemovedFrom(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.RemovedFrom(location, player); if (_CardBoughtHandler != null) player.CardBought -= _CardBoughtHandler; _CardBoughtHandler = null; } } public class TradeRoute : Card { public TradeRoute() : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.Component | Traits.DeckReduction | Traits.PlusCoin | Traits.PlusBuy | Traits.Trasher | Traits.RemoveCurses | Traits.Terminal | Traits.ConditionalBenefit | Traits.RemoveFromHand, edition: Edition.First) { BaseCost = new Cost(3); Benefit.Buys = 1; } public override bool Finalize(IGame game, ISupply supply) { Contract.Requires(game != null, "game cannot be null"); if (base.Finalize(game, supply)) return true; foreach (var sVictory in game.Table.TableEntities.Values.OfType()) { if (sVictory.Randomizer.Category.HasFlag(Categories.Victory)) sVictory.AddToken(new TradeRouteToken()); } game.Table.TokenPiles[TypeClass.TradeRouteToken] = new TokenCollection(); return false; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var benefit = new CardBenefit(); benefit.Currency += new Coin(player._Game.Table.TokenPiles[TypeClass.TradeRouteToken].Count); player.ReceiveBenefit(this, benefit); var choice = new Choice(Resource.TrashACard, this, player.Hand, ChoiceOutcome.Trash, player); var result = player.MakeChoice(choice); if (result.Cards.Any()) player.Trash(this, player.RetrieveCardFrom(DeckLocation.Hand, result.Cards[0])); } } public class TradeRouteToken : Token { public bool Used { get; private set; } public TradeRouteToken() : base("TR", "Trade Route token") { } public override bool Gaining() { if (!Used) { Used = true; return true; } return false; } public override string Title => "Once a card is gained from this supply pile, this token will get added to the Trade Route Mat"; } public class Vault : Card { public Vault() : this(Edition.First) { } public Vault(Edition edition) : base( Categories.Action, Source.Prosperity, Location.Kingdom, Traits.PlusCard | Traits.PlusCoin | Traits.Discard | Traits.AffectOthers | Traits.Terminal | Traits.ConditionalBenefit | Traits.NetCardDraw, edition: edition) { BaseCost = new Cost(5); Benefit.Cards = 2; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); var choice = new Choice("Discard any number of cards. +1 per card discarded.", this, player.Hand, ChoiceOutcome.Discard, player, minimum: 0, maximum: player.Hand.Count); var result = player.MakeChoice(choice); player.Discard(DeckLocation.Hand, result.Cards); var benefit = new CardBenefit(); benefit.Currency += new Coin(result.Cards.Count); player.ReceiveBenefit(this, benefit); var enumerator = player._Game.GetPlayersStartingWithEnumerator(player); enumerator.MoveNext(); // skip active player while (enumerator.MoveNext()) { var otherPlayer = enumerator.Current; if (otherPlayer.Hand.Count >= 2) { var choicePlayer = Choice.CreateYesNoChoice("Do you want to discard 2 cards to draw 1 card?", this, otherPlayer); var resultPlayer = otherPlayer.MakeChoice(choicePlayer); if (resultPlayer.Options[0] == Resource.Yes) { var choiceDiscard = new Choice(Resource.Discard2Cards, this, otherPlayer.Hand, ChoiceOutcome.Discard, otherPlayer, minimum: 2, maximum: 2); var discards = otherPlayer.MakeChoice(choiceDiscard); otherPlayer.Discard(DeckLocation.Hand, discards.Cards); if (otherPlayer.CanDraw) otherPlayer.DrawHand(1); } } } Benefit.Currency.Coin.Value = 0; } } public class Venture : Card { public Venture() : base(Categories.Treasure, Source.Prosperity, Location.Kingdom, Traits.PlusCard | Traits.PlusCoin | Traits.Discard) { BaseCost = new Cost(5); Benefit.Currency.Coin.Value = 1; } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); player.BeginDrawing(); while (player.Revealed[Categories.Treasure].Count < 1 && player.CanDraw) player.Draw(DeckLocation.Revealed); player.EndDrawing(); var cards = player.Revealed[Categories.Treasure]; player.DiscardRevealed(c => !cards.Contains(c)); if (cards.Any()) player.PlayCardInternal(cards[0]); } } public class VictoryToken : Token { public VictoryToken() : base("", "Victory Point chit") { } public override string Title => "Worth 1 at the end of the game"; } public class Watchtower : Card { private CardGainedEventHandler _CardGainedHandler; public Watchtower() : this(Edition.First) { } public Watchtower(Edition edition) : base(Categories.Action | Categories.Reaction, Source.Prosperity, Location.Kingdom, Traits.ReactToGain | Traits.Defense | Traits.CardOrdering | Traits.PlusCard | Traits.Trasher | Traits.Terminal | Traits.NetCardDraw, edition: edition) { BaseCost = new Cost(3); } public override void FollowInstructions(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.FollowInstructions(player); while (player.Hand.Count < 6 && player.CanDraw) player.DrawHand(1); } public override void AddedTo(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.AddedTo(location, player); if (location == DeckLocation.Hand) { if (_CardGainedHandler != null) player.CardGained -= _CardGainedHandler; _CardGainedHandler = new CardGainedEventHandler(Player_CardGained); player.CardGained += _CardGainedHandler; } } private void Player_CardGained(object sender, Players.CardGainEventArgs e) { var player = sender as IPlayer; var key = Type.ToString(); // Already been cancelled -- don't need to process this one // If the card has been "lost track of", then we can skip revealing it // We also need to make sure we're in the player's hand and we can be revealed if (e.Cancelled || e.IsLostTrackOf || !player.Hand.Contains(PhysicalCard) || e.Resolvers.ContainsKey(key)) return; e.Resolvers[key] = new CardGainResolver(player, this, "RevealCard", Resource.RevealCard.Replace("{card}", PhysicalCard.ToString()), Player_RevealWatchtower, false); } internal void Player_RevealWatchtower(IPlayer player, ref Players.CardGainEventArgs e) { player.AddCardInto(DeckLocation.Revealed, player.RetrieveCardFrom(DeckLocation.Hand, PhysicalCard)); player.AddCardInto(DeckLocation.Hand, player.RetrieveCardFrom(DeckLocation.Revealed, PhysicalCard)); var trashChoice = new Choice(Resource.TrashOrTopDeck.Replace("{card}", e.Card.ToString()), this, e.Card, new List { Resource.Trash, Resource.PutOnDeck }, player); var trashResult = player.MakeChoice(trashChoice); if (trashResult.Options[0] == Resource.Trash) { e.Cancelled = true; var card = player.RetrieveCardFrom(e.Location, e.Card); if (card != null) player.Trash(this, e.Card); e.IsLostTrackOf = true; } else { e.Cancelled = true; if (e.Location != DeckLocation.Deck && e.Position != DeckPosition.Top) { player.RetrieveCardFrom(e.Location, e.Card); e.Location = DeckLocation.Deck; e.Position = DeckPosition.Top; } } e.HandledBy.Add(Type); } public override void RemovedFrom(DeckLocation location, IPlayer player) { Contract.Requires(player != null, "player cannot be null"); base.RemovedFrom(location, player); if (_CardGainedHandler != null) player.CardGained -= _CardGainedHandler; _CardGainedHandler = null; } } public class WorkersVillage : Card { public WorkersVillage() : base(Categories.Action, Source.Prosperity, Location.Kingdom, Traits.PlusCard | Traits.PlusAction | Traits.PlusMultipleActions | Traits.PlusBuy | Traits.Cantrip) { BaseCost = new Cost(4); Benefit.Actions = 2; Benefit.Cards = 1; Benefit.Buys = 1; } } }