using System; using System.Collections.Generic; using System.Linq; using System.Text; using DominionBase.Cards; using DominionBase.Piles; namespace DominionBase.Players.AI { public class Standard : Basic { public new static String AIName { get { return "Standard"; } } public new static String AIDescription { get { return "Baseline performance AI that makes as good of decisions as it can, but has no strong focus for buying."; } } public static Boolean IsDownloading = false; private double potionLikelihood = 1f; private Dictionary KnownPlayerHands = new Dictionary(); public Standard(Game game, String name) : base(game, name) { potionLikelihood += Utilities.Gaussian.NextGaussian() / 10; OpeningProcessor proc = OpeningProcessor.Acquire(); proc.Run(); OpeningProcessor.Release(); foreach (Player player in game.Players) { // Skip myself -- we already know 100% info & don't care anyway if (player == this) continue; KnownPlayerHands[player] = new CardCollection(); player.TurnEnded += new TurnEndedEventHandler(otherPlayer_TurnEnded); player.CardsDiscarded += new CardsDiscardedEventHandler(otherPlayer_CardsDiscarded); player.Revealed.PileChanged += new Pile.PileChangedEventHandler(otherPlayer_CardsRevealed_PileChanged); player.Hand.PileChanged += new Pile.PileChangedEventHandler(otherPlayer_CardsHand_PileChanged); } } internal override void TearDown() { base.TearDown(); foreach (Player player in this._Game.Players) { if (player == this) continue; player.TurnEnded -= new TurnEndedEventHandler(otherPlayer_TurnEnded); player.CardsDiscarded -= new CardsDiscardedEventHandler(otherPlayer_CardsDiscarded); player.Revealed.PileChanged -= new Pile.PileChangedEventHandler(otherPlayer_CardsRevealed_PileChanged); player.Hand.PileChanged -= new Pile.PileChangedEventHandler(otherPlayer_CardsHand_PileChanged); } } void otherPlayer_TurnEnded(object sender, TurnEndedEventArgs e) { KnownPlayerHands[e.Player] = new CardCollection(); } void otherPlayer_CardsRevealed_PileChanged(object sender, PileChangedEventArgs e) { KnownPlayerHands[e.Player].AddRange(e.AddedCards); } void otherPlayer_CardsHand_PileChanged(object sender, PileChangedEventArgs e) { if (e.OperationPerformed == PileChangedEventArgs.Operation.Added) { // We can add red-backed cards (Stash) to the known cards when added to a player's hand KnownPlayerHands[e.Player].AddRange(e.AddedCards.Where(c => c.CardBack == CardBack.Red)); } else if (e.OperationPerformed == PileChangedEventArgs.Operation.Removed) { foreach (Card card in e.RemovedCards.Where(c => c.CardBack == CardBack.Red)) { if (KnownPlayerHands[e.Player].Contains(card)) KnownPlayerHands[e.Player].Remove(card); } } } void otherPlayer_CardsDiscarded(object sender, CardsDiscardEventArgs e) { if (!KnownPlayerHands.ContainsKey(sender as Player)) return; // We shouldn't cheat -- only the last card should be visible Card lastCard = e.Cards.LastOrDefault(); if (lastCard == null) return; // Remove the card if it's found in our list of cards that we know about Card foundCard = KnownPlayerHands[sender as Player].FirstOrDefault(c => c.Name == lastCard.Name); if (foundCard != null) KnownPlayerHands[sender as Player].Remove(foundCard); } protected override Card FindBestCardToPlay(IEnumerable cards) { // Sort the cards by cost (potion = 2.5 * coin) // Also, use a cost of 7 for Prize cards (since they have no cost normally) cards = cards.Where(card => this.ShouldPlay(card)).OrderByDescending( card => (card.Category & Category.Prize) == Category.Prize ? 7 : (card.BaseCost.Coin.Value + 2.5 * card.BaseCost.Potion.Value)); // Always play King's Court if there is one (?) Card kc = cards.FirstOrDefault(card => card.CardType == Cards.Prosperity.TypeClass.KingsCourt); if (kc != null) { // Not quite -- Don't play KC if there are certain cards where it's detrimental, or at least not helpful, to play multiple times // Also, to not be hurtful in certain situations, disallow KC'ing certain cards like Island if (!cards.All(c => c.CardType == Cards.Base.TypeClass.Chapel || c.CardType == Cards.Base.TypeClass.Library || c.CardType == Cards.Base.TypeClass.Remodel || c.CardType == Cards.Intrigue.TypeClass.SecretChamber || c.CardType == Cards.Intrigue.TypeClass.Upgrade || c.CardType == Cards.Seaside.TypeClass.Island || c.CardType == Cards.Seaside.TypeClass.Lookout || c.CardType == Cards.Seaside.TypeClass.Outpost || c.CardType == Cards.Seaside.TypeClass.Salvager || c.CardType == Cards.Seaside.TypeClass.Tactician || c.CardType == Cards.Seaside.TypeClass.TreasureMap || c.CardType == Cards.Prosperity.TypeClass.CountingHouse || c.CardType == Cards.Prosperity.TypeClass.Forge || c.CardType == Cards.Prosperity.TypeClass.TradeRoute || c.CardType == Cards.Prosperity.TypeClass.Watchtower || c.CardType == Cards.Cornucopia.TypeClass.Remake || c.CardType == Cards.Hinterlands.TypeClass.Develop)) return kc; } // Always play Throne Room if there is one (?) Card tr = cards.FirstOrDefault(card => card.CardType == Cards.Base.TypeClass.ThroneRoom); if (tr != null) { // Not quite -- Don't play TR if there are certain cards where it's detrimental, or at least not helpful, to play multiple times // Also, to not be hurtful in certain situations, disallow TR'ing certain cards like Island if (!cards.All(c => c.CardType == Cards.Base.TypeClass.Chapel || c.CardType == Cards.Base.TypeClass.Library || c.CardType == Cards.Base.TypeClass.Remodel || c.CardType == Cards.Intrigue.TypeClass.SecretChamber || c.CardType == Cards.Intrigue.TypeClass.Upgrade || c.CardType == Cards.Seaside.TypeClass.Island || c.CardType == Cards.Seaside.TypeClass.Lookout || c.CardType == Cards.Seaside.TypeClass.Outpost || c.CardType == Cards.Seaside.TypeClass.Salvager || c.CardType == Cards.Seaside.TypeClass.Tactician || c.CardType == Cards.Seaside.TypeClass.TreasureMap || c.CardType == Cards.Prosperity.TypeClass.CountingHouse || c.CardType == Cards.Prosperity.TypeClass.Forge || c.CardType == Cards.Prosperity.TypeClass.TradeRoute || c.CardType == Cards.Prosperity.TypeClass.Watchtower || c.CardType == Cards.Cornucopia.TypeClass.Remake || c.CardType == Cards.Hinterlands.TypeClass.Develop)) return tr; } // Play Menagerie first if we've got a hand with only unique cards if (cards.Count(c => c.CardType == Cards.Cornucopia.TypeClass.Menagerie) == 1) { IEnumerable typesMenagerie = this.Hand.Select(c => c.CardType); if (typesMenagerie.Count() == typesMenagerie.Distinct().Count()) return cards.First(c => c.CardType == Cards.Cornucopia.TypeClass.Menagerie); } // Keep Shanty Town available to play only if any the following criteria are satisfied if (cards.Count(c => c.CardType == Cards.Intrigue.TypeClass.ShantyTown) > 0) { if (cards.Count(c => c.CardType != Cards.Intrigue.TypeClass.ShantyTown) == 0 || // No other Action cards in hand cards.Count(c => c.CardType == Cards.Intrigue.TypeClass.ShantyTown) > 1 || // At least 1 other Shanty Town in hand (this.Actions == 1 && cards.Count(c => this.ShouldPlay(c) && c.Benefit.Actions == 0) >= 2) || // 1 Action left & 2 or more Terminal Actions (this.Actions == 1 && cards.Count(c => this.ShouldPlay(c) && c.Benefit.Actions == 0 && c.Benefit.Cards > 0) >= 1) || // 1 Action left & 1 or more card-drawing Actions this.Hand[Cards.Cornucopia.TypeClass.HornOfPlenty].Count > 0) // Horn of Plenty in hand { // Keep it. Criteria has been satisfied } else cards = cards.Where(c => c.CardType != Cards.Intrigue.TypeClass.ShantyTown); } Card plusActions = null; if (this.CurrentTurn.CardsPlayed.Count(c => (c.Category & Category.Action) == Category.Action) >= 2) plusActions = cards.FirstOrDefault(c => c.CardType == Cards.Intrigue.TypeClass.Conspirator); if (plusActions == null) plusActions = cards.FirstOrDefault(card => card.Benefit.Actions > 0); if (plusActions != null) return plusActions; Turn previousTurn = null; if (this._Game.TurnsTaken.Count > 1) previousTurn = this._Game.TurnsTaken[this._Game.TurnsTaken.Count - 2]; // Play Smugglers if the player to our right gained a card costing at least 5 (that we can gain as well) // Only do this about 40% of the time. It's pretty lame, man! if (cards.Any(card => card.CardType == Cards.Seaside.TypeClass.Smugglers) && previousTurn != null && previousTurn.CardsGained.Any(card => _Game.Cost(card).Potion == 0 && card.BaseCost.Coin >= 5 && this._Game.Table.Supplies.ContainsKey(card) && this._Game.Table.Supplies[card].CanGain()) && random.Next(0, 5) < 2) return cards.First(card => card.CardType == Cards.Seaside.TypeClass.Smugglers); if (this.Hand[Category.Curse].Count > 0) { // Play an Ambassador card if there is one and we have at least 1 Curse in our hand Card trasher = cards.FirstOrDefault( card => card.CardType == Cards.Base.TypeClass.Chapel || card.CardType == Cards.Base.TypeClass.Remodel || card.CardType == Cards.Seaside.TypeClass.Ambassador || card.CardType == Cards.Seaside.TypeClass.Salvager || card.CardType == Cards.Alchemy.TypeClass.Apprentice || card.CardType == Cards.Prosperity.TypeClass.Expand || card.CardType == Cards.Prosperity.TypeClass.Forge || card.CardType == Cards.Prosperity.TypeClass.TradeRoute || (card.CardType == Cards.Cornucopia.TypeClass.Remake && this.Hand[Category.Curse].Count > 1) || card.CardType == Cards.Hinterlands.TypeClass.Develop || card.CardType == Cards.Hinterlands.TypeClass.JackOfAllTrades || card.CardType == Cards.Hinterlands.TypeClass.Trader); if (trasher != null) return trasher; } // Don't play Trader if: // A) the current game progress is greater than 0.75 (still early) and either // 1) there are no Coppers in hand or // 2)the number of playable Treasure cards better than Copper is still small // B) The lowest-cost non-Victory card we have in hand is >= 3 if (cards.Count(c => c.CardType == Cards.Hinterlands.TypeClass.Trader) > 0) { if (this.GameProgress > 0.75 && (this.Hand[Cards.Universal.TypeClass.Copper].Count == 0 || this.CountAll(this, c => (c.Category & Category.Treasure) == Category.Treasure && c.CardType != Cards.Universal.TypeClass.Copper, true, false) < 3)) cards = cards.Where(c => c.CardType != Cards.Hinterlands.TypeClass.Trader); else if (this.Hand.Count(c => (c.Category & Category.Victory) != Category.Victory && _Game.Cost(c).Coin.Value < 3) == 0) cards = cards.Where(c => c.CardType != Cards.Hinterlands.TypeClass.Trader); } // Don't play Courtyard if there are fewer than 2 cards to draw if (cards.Count(c => c.CardType == Cards.Intrigue.TypeClass.Courtyard) > 0 && this.CountAll(this, c => true, true, true) < 2) cards = cards.Where(c => c.CardType != Cards.Intrigue.TypeClass.Courtyard); if (cards.Count() > 0) // Just play the most expensive one return cards.ElementAt(0); return null; } protected override CardCollection FindBestCardsToPlay(IEnumerable cards) { // Play all Contrabands first if we can Card contraband = cards.FirstOrDefault(c => c.CardType == Cards.Prosperity.TypeClass.Contraband); if (contraband != null) return new CardCollection() { contraband }; // Play all Counterfeits next Card counterfeit = cards.FirstOrDefault(c => c.CardType == Cards.DarkAges.TypeClass.Counterfeit); if (counterfeit != null) return new CardCollection() { counterfeit }; // Play all normal treasures next IEnumerable normalTreasures = cards.Where(c => (c.Category & Category.Treasure) == Category.Treasure && c.CardType != Cards.Prosperity.TypeClass.Bank && c.CardType != Cards.Prosperity.TypeClass.Loan && c.CardType != Cards.Prosperity.TypeClass.Venture && c.CardType != Cards.Cornucopia.TypeClass.HornOfPlenty && c.CardType != Cards.Hinterlands.TypeClass.IllGottenGains); if (normalTreasures.Count() > 0) return new CardCollection(normalTreasures); // Only play Loan & Venture after cards like Philosopher's Stone that work better with more cards // There are some very specific situations where playing Horn Of Plenty before Philospher's Stone // or Venture is the right way to play things, but that's so incredibly rare. IEnumerable loanVenture = cards.Where(c => c.CardType == Cards.Prosperity.TypeClass.Loan || c.CardType == Cards.Prosperity.TypeClass.Venture); if (loanVenture.Count() > 0) return new CardCollection(loanVenture); // Play Ill-Gotten Gains later so we can figure out if we need that extra Copper Card illGottenGains = cards.FirstOrDefault(c => c.CardType == Cards.Hinterlands.TypeClass.IllGottenGains); if (illGottenGains != null) return new CardCollection() { illGottenGains }; // Always play Bank & Horn of Plenty last IEnumerable bankHornOfPlenty = cards.Where(c => c.CardType == Cards.Prosperity.TypeClass.Bank || c.CardType == Cards.Cornucopia.TypeClass.HornOfPlenty); foreach (Card card in bankHornOfPlenty) { if (card.CardType == Cards.Cornucopia.TypeClass.HornOfPlenty) { List cardTypes = new List() { Cards.Cornucopia.TypeClass.HornOfPlenty }; foreach (Card c in this.Tableau) { Type t = c.CardType; if (!cardTypes.Contains(t)) cardTypes.Add(t); } foreach (Card c in this.PreviousTableau) { Type t = c.CardType; if (!cardTypes.Contains(t)) cardTypes.Add(t); } // Don't even bother playing Horn of Plenty if there's nothing for me to gain except Copper & Estate if (this._Game.Table.Supplies.Values.FirstOrDefault( s => s.CardType != Cards.Universal.TypeClass.Copper && s.CardType != Cards.Universal.TypeClass.Estate && s.CanGain() && this.ShouldBuy(s) && s.CurrentCost <= new Cost(cardTypes.Count)) == null) continue; } return new CardCollection() { card }; } // Don't play anything if we've fallen to here. return new CardCollection(); } protected override bool ShouldBuy(Type type) { if (type == Cards.Universal.TypeClass.Curse) return false; else if (type == Cards.Base.TypeClass.Moneylender) return false; else if (type == Cards.Base.TypeClass.Chapel) return false; else if (type == Cards.Base.TypeClass.Remodel) return false; else if (type == Cards.Intrigue.TypeClass.Upgrade) return false; else if (type == Cards.Intrigue.TypeClass.TradingPost) return false; else if (type == Cards.Intrigue.TypeClass.Masquerade) return false; else if (type == Cards.Intrigue.TypeClass.Coppersmith) return false; else if (type == Cards.Seaside.TypeClass.Lookout) return false; else if (type == Cards.Seaside.TypeClass.Ambassador) return false; else if (type == Cards.Seaside.TypeClass.Navigator) return false; else if (type == Cards.Seaside.TypeClass.Salvager) return false; else if (type == Cards.Seaside.TypeClass.TreasureMap) return false; //else if (type == Cards.Alchemy.TypeClass.Potion) // return false; //else if (type == Cards.Alchemy.TypeClass.Possession) // return false; else if (type == Cards.Alchemy.TypeClass.Apprentice) return false; else if (type == Cards.Alchemy.TypeClass.Transmute) return false; else if (type == Cards.Prosperity.TypeClass.Expand) return false; else if (type == Cards.Prosperity.TypeClass.Forge) return false; else if (type == Cards.Prosperity.TypeClass.TradeRoute) return false; else if (type == Cards.Cornucopia.TypeClass.Remake) return false; else if (type == Cards.Hinterlands.TypeClass.Develop) return false; else if (type == Cards.DarkAges.TypeClass.Forager) return false; else if (type == Cards.DarkAges.TypeClass.Procession) return false; else if (type == Cards.DarkAges.TypeClass.Rats) return false; else if (type == Cards.DarkAges.TypeClass.Rebuild) return false; else if (type == Cards.DarkAges.TypeClass.RuinsSupply) return false; return true; } protected override Supply FindBestCardToBuy(List buyableSupplies) { // If this is the first 2 turns of the game, let's do an opening if (this._Game.TurnsTaken.TurnNumber(this) <= 2) { OpeningProcessor proc = OpeningProcessor.Acquire(); IEnumerable availableOpenings = proc.Openings.FilterFor(this._Game.Table.Supplies.Select(s => s.Value)); availableOpenings = availableOpenings.Where(o => o.Skill > 0); availableOpenings = availableOpenings.Where(o => o.Cards.Any(c => buyableSupplies.Any(bs => bs.CardType == c.CardType))); OpeningProcessor.Release(); } //// Buy a potion if we can (??) -- Let's only do this 1/2 the time //if (this.Currency != null && (this.Currency.Coin == 4 && this.Currency.Potion == 0 && random.Next(2) == 0)) //{ // Supply potion = buyableSupplies.SingleOrDefault(supply => supply.CardType == Cards.Alchemy.TypeClass.Potion); // if (potion != null) // return potion; //} Dictionary> scores = new Dictionary>(); foreach (Supply supply in buyableSupplies) { // We need to compute score based on the original/base cost of the card double score = supply.BaseCost.Coin.Value + 2.5f * supply.BaseCost.Potion.Value; // Scale based on the original -vs- current cost -- cheaper cards should be more valuable to us! score += Math.Log((score + 1) / (supply.CurrentCost.Coin.Value + 2.5f * supply.CurrentCost.Potion.Value + 1)); if (!ShouldBuy(supply)) score = -1d; // Scale back the score accordingly if it's near the end of the game and the card is not a Victory card if (this.GameProgress < 0.25 && (supply.Category & Category.Victory) != Category.Victory) score *= 0.15d; // Never buy non-Province/Colony/Farmland/Tunnel Victory-only cards early if (this.GameProgress > 0.71 && supply.Category == Category.Victory && supply.CardType != Cards.Universal.TypeClass.Province && supply.CardType != Cards.Prosperity.TypeClass.Colony && supply.CardType != Cards.Hinterlands.TypeClass.Farmland && supply.CardType != Cards.Hinterlands.TypeClass.Tunnel) score = -1d; if ((this.GameProgress > 0.25 && supply.CardType == Cards.Universal.TypeClass.Estate) || (this.GameProgress > 0.25 && supply.CardType == Cards.Alchemy.TypeClass.Vineyard) || (this.GameProgress > 0.35 && supply.CardType == Cards.Base.TypeClass.Gardens) || (this.GameProgress > 0.35 && supply.CardType == Cards.Hinterlands.TypeClass.SilkRoad) || (this.GameProgress > 0.35 && supply.CardType == Cards.DarkAges.TypeClass.Feodum) || (this.GameProgress > 0.4 && supply.CardType == Cards.Universal.TypeClass.Duchy) || (this.GameProgress > 0.4 && supply.CardType == Cards.Intrigue.TypeClass.Duke) || (this.GameProgress > 0.4 && supply.CardType == Cards.Cornucopia.TypeClass.Fairgrounds)) score *= 0.15d; // Duke/Duchy decision if (supply.CardType == Cards.Intrigue.TypeClass.Duke || supply.CardType == Cards.Universal.TypeClass.Duchy) { int duchies = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Duchy, false, false); int dukes = this.CountAll(this, c => c.CardType == Cards.Intrigue.TypeClass.Duke, false, false); // If gaining a Duke is not as useful as gaining a Duchy, don't get the Duke if (supply.CardType == Cards.Intrigue.TypeClass.Duke && (3 + dukes + 1) * duchies < (3 + dukes) * (duchies + 1)) score *= 0.95d; // If gaining a Duchy is not as useful as gaining a Duke, don't get the Duchy if (supply.CardType == Cards.Universal.TypeClass.Duchy && (3 + dukes + 1) * duchies > (3 + dukes) * (duchies + 1)) score *= 0.95d; } // Scale Feodum score based on how many Silvers we have if (supply.CardType == Cards.DarkAges.TypeClass.Feodum) { int totalSilverCount = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Silver, true, false); score *= Math.Pow(1.045, 2 * (totalSilverCount - 6)); } if (supply.CardType == Cards.Universal.TypeClass.Copper) { // Never buy Copper cards unless we have a Goons in play if (this.Tableau[Cards.Prosperity.TypeClass.Goons].Count == 0) score = -1d; //else if (this.CurrentTurn.CardsBought.Count(c => c.CardType == Cards.Universal.TypeClass.Copper) > 1) // score = -1d; } if (supply.CardType == Cards.Universal.TypeClass.Silver) { //int totalSilverCount = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Silver, true, false); int totalFeodumCount = this.CountAll(this, c => c.CardType == Cards.DarkAges.TypeClass.Feodum, false, false); score *= Math.Pow(1.04, 2 * totalFeodumCount); if (totalFeodumCount > 0 && this.GameProgress < 0.25) score /= Math.Max(this.GameProgress, 0.08); } // Early on, and when we don't have many Silver, give a slight bias to Silvers //if (supply.CardType == Cards.Universal.TypeClass.Silver) //{ // score *= Math.Pow(1.04, 2 * Math.Pow(this.GameProgress, 2)); // int silverCount = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Silver, true, false); // int copperCount = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false); // int treasureCount = this.CountAll(this, c => (c.Category & Category.Treasure) == Category.Treasure, true, false); // int totalCount = this.CountAll(); // //if ( // //score *= //} if (supply.CardType == Cards.Base.TypeClass.Moat) { IEnumerable attackSupplies = this._Game.Table.Supplies.Values.Where( s => (s.Category & Category.Attack) == Category.Attack); int attackCardsLeft = attackSupplies.Sum(s => s.Count); int attackCardsInTrash = this._Game.Table.Trash.Count(c => attackSupplies.Any(s => s.CardType == c.CardType)); int attackCardsInDecks = attackSupplies.Sum(s => s.OriginalCount) - attackCardsLeft - attackCardsInTrash; int attackCardsInMyDeck = CountAll(this, c => (c.Category & Category.Attack) == Category.Attack, true, false); score *= Math.Pow(1.05, attackCardsInDecks - attackCardsInMyDeck + 0.25 * attackCardsLeft); } if (supply.CardType == Cards.Seaside.TypeClass.Lighthouse) { IEnumerable attackSupplies = this._Game.Table.Supplies.Values.Where( s => (s.Category & Category.Attack) == Category.Attack); int attackCardsLeft = attackSupplies.Sum(s => s.Count); int attackCardsInTrash = this._Game.Table.Trash.Count(c => attackSupplies.Any(s => s.CardType == c.CardType)); int attackCardsInDecks = attackSupplies.Sum(s => s.OriginalCount) - attackCardsLeft - attackCardsInTrash; int attackCardsInMyDeck = CountAll(this, c => (c.Category & Category.Attack) == Category.Attack, true, false); score *= Math.Pow(1.05, attackCardsInDecks - attackCardsInMyDeck + 0.25 * attackCardsLeft); } // Embargo tokens -- these make cards worth less than they would normally be (but only if there are Curse cards left) if (supply.Tokens.Count(token => token.GetType() == Cards.Seaside.TypeClass.EmbargoToken) > 0 && this._Game.Table.Curse.Count > 0) { score *= Math.Pow(0.8, Math.Min(supply.Tokens.Count(token => token.GetType() == Cards.Seaside.TypeClass.EmbargoToken), this._Game.Table.Curse.Count)); // If Estates are Embargoed (and there are Curses left), make Estates not very nice to buy if (supply.CardType == Cards.Universal.TypeClass.Estate) score = 0.01; } // Peddler -- not really worth 8; scale it back if it's over 5 if (supply.CardType == Cards.Prosperity.TypeClass.Peddler) { if (supply.CurrentCost.Coin > 5) score *= 0.6f; } if (supply.CardType == Cards.Prosperity.TypeClass.Mint) { int totalTreasureCards = this.CountAll(this, c => (c.Category & Category.Treasure) == Category.Treasure, true, false); int totalCopperCards = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false); IEnumerable cardsToTrash = this.PreviousTableau[Category.Treasure].Union(this.Tableau[Category.Treasure]); double cardWorth = 0d; foreach (Card card in cardsToTrash) { // Since Diadem has no "Cost", we have to create an artificial worth for it if (card.CardType == Cards.Cornucopia.TypeClass.Diadem) cardWorth += 7d; // Copper has a slightly negative worth else if (card.CardType == Cards.Universal.TypeClass.Copper) cardWorth += -0.25; // Ill-Gotten Gains's worth is significantly less after it's been gained else if (card.CardType == Cards.Hinterlands.TypeClass.IllGottenGains) cardWorth -= 1.72; else cardWorth += card.BaseCost.Coin.Value + 2.5 * card.BaseCost.Potion.Value; } score *= Math.Pow(0.975, cardWorth); if (totalTreasureCards - totalCopperCards < cardsToTrash.Count()) score *= 0.85; } // Scale Potion likelihood based on how many we already have -- don't want a glut of them! // Also scale Potion likelihood based on the number of Potion-costing cards in the Supply -- More = bettar if (supply.CardType == Cards.Alchemy.TypeClass.Potion) { score *= potionLikelihood; score *= Math.Pow(0.8, this.CountAll(this, c => c.CardType == Cards.Alchemy.TypeClass.Potion, true, false)); score *= Math.Pow(1.1, this._Game.Table.Supplies.Values.Count(s => s.BaseCost.Potion > 0)); } // Don't overemphasize Throne Room or King's Court if we don't have many Action cards compared to our deck size if (supply.CardType == Cards.Base.TypeClass.ThroneRoom || supply.CardType == Cards.Prosperity.TypeClass.KingsCourt) { int nonTR_KC_actions = this.CountAll(this, c => (c.Category & Category.Action) == Category.Action && c.CardType != Cards.Base.TypeClass.ThroneRoom && c.CardType != Cards.Prosperity.TypeClass.KingsCourt, true, false); // Fudge factor nonTR_KC_actions = (int)(nonTR_KC_actions * (1.0 + Utilities.Gaussian.NextGaussian() / 8d)); int totalCards = this.CountAll(); // Fudge factor totalCards = (int)(totalCards * (1.0 + Utilities.Gaussian.NextGaussian() / 8d)); double ratio = (double)nonTR_KC_actions / totalCards; score *= Math.Sqrt(ratio / 0.18d); } // Don't overemphasize Golem if we don't have many non-Golem Action cards if (supply.CardType == Cards.Alchemy.TypeClass.Golem) { int nonGolem_actions = this.CountAll(this, c => (c.Category & Category.Action) == Category.Action && c.CardType != Cards.Alchemy.TypeClass.Golem, true, false); // Fudge factor nonGolem_actions = (int)(nonGolem_actions * (1.0 + Utilities.Gaussian.NextGaussian() / 8d)); int totalCards = this.CountAll(); // Fudge factor totalCards = (int)(totalCards * (1.0 + Utilities.Gaussian.NextGaussian() / 8d)); double ratio = (double)nonGolem_actions / totalCards; score *= Math.Sqrt(ratio / 0.18d); } if (supply.CardType == Cards.Seaside.TypeClass.Embargo) { IEnumerable curseGivingSupplies = this._Game.Table.Supplies.Values.Where( s => s.CardType == Cards.Base.TypeClass.Witch || s.CardType == Cards.Intrigue.TypeClass.Swindler || s.CardType == Cards.Intrigue.TypeClass.Torturer || s.CardType == Cards.Seaside.TypeClass.Ambassador || s.CardType == Cards.Seaside.TypeClass.SeaHag || s.CardType == Cards.Alchemy.TypeClass.Familiar || s.CardType == Cards.Prosperity.TypeClass.Mountebank || s.CardType == Cards.Cornucopia.TypeClass.Jester || s.CardType == Cards.Cornucopia.TypeClass.YoungWitch); int curseGivingCardsLeft = curseGivingSupplies.Sum(s => s.Count); int curseGivingCardsInTrash = this._Game.Table.Trash.Count(c => curseGivingSupplies.Any(s => s.CardType == c.CardType)); int curseGivingCardsInDecks = curseGivingSupplies.Sum(s => s.OriginalCount) - curseGivingCardsLeft - curseGivingCardsInTrash; int curseGivingCardsInMyDeck = CountAll(this, c => c.CardType == Cards.Base.TypeClass.Witch || c.CardType == Cards.Intrigue.TypeClass.Swindler || c.CardType == Cards.Intrigue.TypeClass.Torturer || c.CardType == Cards.Seaside.TypeClass.Ambassador || c.CardType == Cards.Seaside.TypeClass.SeaHag || c.CardType == Cards.Alchemy.TypeClass.Familiar || c.CardType == Cards.Prosperity.TypeClass.Mountebank || c.CardType == Cards.Cornucopia.TypeClass.Jester || c.CardType == Cards.Cornucopia.TypeClass.YoungWitch || c.CardType == Cards.Cornucopia.TypeClass.Followers, true, false); // Followers is either in Supply pile or player's piles if (this._Game.Table.Supplies.ContainsKey(Cards.Cornucopia.TypeClass.PrizeSupply)) { int followersInSupply = this._Game.Table.Supplies[Cards.Cornucopia.TypeClass.PrizeSupply].Count(c => c.CardType == Cards.Cornucopia.TypeClass.Followers); int followersInTrash = this._Game.Table.Trash.Count(c => c.CardType == Cards.Cornucopia.TypeClass.Followers); curseGivingCardsLeft += followersInSupply; if (followersInSupply == 0 && followersInTrash == 0) curseGivingCardsInDecks += 1; } // TODO : This still needs some work -- I need to come up with a way that scales the "worth" of Embargo cards // based on the number of Curses left and the number of Curse-giving cards in play as well as remaining //double alphaTerm = 1.0 / (0.5 * Math.Log((double)(curseGivingCardsInDecks - curseGivingCardsInMyDeck + 1)) + 1.0); double gammaTerm = 0.5f; if (curseGivingCardsInMyDeck < 30) gammaTerm = Math.Max(1.0 - (Math.Pow(1.0 / (30.0 - curseGivingCardsInMyDeck), 0.25) - Math.Pow(1 / 30.0, 0.25)), gammaTerm); //double deltaTerm = Math.Log((double)(this._Game.Table.Supplies[Cards.Universal.TypeClass.Curse].Count + 1)) / 2.5; //score *= alphaTerm * gammaTerm * deltaTerm; //score *= gammaTerm * deltaTerm; score *= 1.1 * gammaTerm; } // Witch gets less & less valuable with fewer & fewer curses, down to about 1.9 when there are none // We should actually make it slightly more likely to get Witch when there are lots of curses if (supply.CardType == Cards.Base.TypeClass.Witch) { score *= Math.Pow(0.8, (9.8d - (1d + Utilities.Gaussian.NextGaussian() / 12d) * Math.Sqrt(10) * Math.Sqrt(((double)this._Game.Table.Curse.Count) / (this._Game.Players.Count - 1))) * 4.3335 / 10); } // Seahag gets less & less valuable with fewer & fewer curses, down to absolutely useless when there are none // We should actually make it slightly more likely to get SeaHag when there are lots of curses if (supply.CardType == Cards.Seaside.TypeClass.SeaHag) { score *= Math.Pow(0.8, 9.8d - (1d + Utilities.Gaussian.NextGaussian() / 12d) * Math.Sqrt(10) * Math.Sqrt(((double)this._Game.Table.Curse.Count) / (this._Game.Players.Count - 1))); } // Familiar gets less & less valuable with fewer & fewer curses, down to about 1.6 when there are none // We should actually make it slightly more likely to get Familiar when there are lots of curses if (supply.CardType == Cards.Alchemy.TypeClass.Familiar) { score *= Math.Pow(0.8, (9.8d - (1d + Utilities.Gaussian.NextGaussian() / 12d) * Math.Sqrt(10) * Math.Sqrt(((double)this._Game.Table.Curse.Count) / (this._Game.Players.Count - 1))) * 5.5335 / 10); } // Limit the number of Contrabands we'll buy to a fairly small amount (1 per every 20 cards or so) if (supply.CardType == Cards.Prosperity.TypeClass.Contraband) { int contrabandsICanPlay = this.CountAll(this, c => c.CardType == Cards.Prosperity.TypeClass.Contraband, true, false); int totalDeckSize = this.CountAll(); double percentageOfContrabands = ((double)contrabandsICanPlay) / totalDeckSize; if (percentageOfContrabands > 0.05) score *= Math.Pow(0.2, Math.Pow(percentageOfContrabands, 2)); } // Limit the number of Outposts we'll buy to a fairly small amount (1 per every 15 cards or so) if (supply.CardType == Cards.Seaside.TypeClass.Outpost) { int outpostsICanPlay = this.CountAll(this, c => c.CardType == Cards.Seaside.TypeClass.Outpost, true, false); int totalDeckSize = this.CountAll(); double percentageOfOutposts = ((double)outpostsICanPlay) / totalDeckSize; if (percentageOfOutposts > 0.6667) score *= Math.Pow(0.2, Math.Pow(percentageOfOutposts, 2)); } // Need to be careful when buying this card, since it can muck up our deck (e.g. making us trash a Province or Colony) if (supply.CardType == Cards.Hinterlands.TypeClass.Farmland) { if (this.Hand.Count == 0) score *= 0.95; else { // We always like trashing Curses if we can, especially when we buy a different Victory card in the process if (this.Hand[Category.Curse].Count > 0) score *= 1.1; // This complicated-looking little nested LINQ comparison checks to see if our hand consists only // of cards that don't have a Supply pile that costs exactly 2 coins more that is gainable // e.g. if we only have Provinces, Colonies, or 5-cost cards in our hand (and no 7-cost Supplies exist), we don't really want to trash them. else if (this.Hand.All(c => !_Game.Table.Supplies.Any(kvp => kvp.Value.CanGain() && kvp.Value.CurrentCost == _Game.Cost(c) + new Currencies.Coin(2)))) { score *= 0.02; } // This LINQ query checks to see if there are any Supply piles that have Victory cards that are // strictly better than any Victory cards we have in our hand (e.g. Province vs. Farmland) else if (this.Hand.Any(c => (c.Category & Category.Victory) == Category.Victory && _Game.Table.Supplies.Any(kvp => (kvp.Value.Category & Category.Victory) == Category.Victory && kvp.Value.VictoryPoints > c.VictoryPoints && kvp.Value.CanGain() && kvp.Value.CurrentCost == _Game.Cost(c) + new Currencies.Coin(2)))) { score *= 1.1; } // I dunno after that... will require testing. In general, be cautious else { score *= 0.75; } } } if (supply.CardType == Cards.Hinterlands.TypeClass.NobleBrigand) { // Never buy Noble Brigand in the first 2 turns, and then be wary of it based on how many Silver & Gold everyone has if (this._Game.TurnsTaken.Count(t => t.Player == this) < 3) score = -1.0; double nbTotalSilverGold = this._Game.Players.Where(p => p != this).Sum(p => p.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Silver || c.CardType == Cards.Universal.TypeClass.Gold, true, false)) / (this._Game.Players.Count - 1); double nbTotalCards = this._Game.Players.Where(p => p != this).Sum(p => p.CountAll()) / (this._Game.Players.Count - 1); if (nbTotalSilverGold < 2 || nbTotalSilverGold / nbTotalCards < 0.1) score *= 0.5; } if (!scores.ContainsKey(score)) scores[score] = new List(); scores[score].Add(supply); } double bestScore = scores.Keys.OrderByDescending(k => k).FirstOrDefault(); if (bestScore >= 0d) return scores[bestScore][random.Next(scores[bestScore].Count)]; return null; } protected override Supply FindBestCardForCost(IEnumerable buyableSupplies, Currency currency, bool buying) { List bestSupplies = new List(); Cost bestCost = null; foreach (Supply supply in buyableSupplies) { if (!ShouldBuy(supply)) continue; // Only return ones we CAN gain if (currency != (Currency)null && currency < supply.CurrentCost) continue; if (bestCost == (Cost)null || (bestCost.Coin.Value + 2.5 * bestCost.Potion.Value) <= (supply.BaseCost.Coin.Value + 2.5 * supply.BaseCost.Potion.Value)) { // Don't buy if it's not a Victory card near the end of the game if (this.GameProgress < 0.25 && (supply.Category & Category.Victory) != Category.Victory) continue; // Never buy Duchies or Estates early if ((this.GameProgress > 0.25 && supply.SupplyCardType == Cards.Universal.TypeClass.Estate) || (this.GameProgress > 0.25 && supply.SupplyCardType == Cards.Alchemy.TypeClass.Vineyard) || (this.GameProgress > 0.4 && supply.SupplyCardType == Cards.Universal.TypeClass.Duchy) || (this.GameProgress > 0.4 && supply.SupplyCardType == Cards.Intrigue.TypeClass.Duke) || (this.GameProgress > 0.4 && supply.SupplyCardType == Cards.Cornucopia.TypeClass.Fairgrounds)) continue; // Duke/Duchy decision if (supply.SupplyCardType == Cards.Intrigue.TypeClass.Duke || supply.SupplyCardType == Cards.Universal.TypeClass.Duchy) { int duchies = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Duchy, false, false); int dukes = this.CountAll(this, c => c.CardType == Cards.Intrigue.TypeClass.Duke, false, false); // If gaining a Duke is not as useful as gaining a Duchy, don't get the Duke if (supply.SupplyCardType == Cards.Intrigue.TypeClass.Duke && (3 + dukes + 1) * duchies < (3 + dukes) * (duchies + 1)) continue; // If gaining a Duchy is not as useful as gaining a Duke, don't get the Duchy if (supply.SupplyCardType == Cards.Universal.TypeClass.Duchy && (3 + dukes + 1) * duchies > (3 + dukes) * (duchies + 1)) continue; } // Reset best cost to new one if (bestCost == (Cost)null || (bestCost.Coin.Value + 2.5 * bestCost.Potion.Value) < (supply.BaseCost.Coin.Value + 2.5 * supply.BaseCost.Potion.Value)) { bestCost = supply.BaseCost; bestSupplies.Clear(); } bestSupplies.Add(supply); } } if (bestSupplies.Count == 0) { foreach (Supply supply in buyableSupplies) { if (supply.SupplyCardType == Cards.Universal.TypeClass.Curse) continue; if (bestCost == (Cost)null || bestCost.Coin.Value <= supply.BaseCost.Coin.Value) { // Reset best cost to new one if (bestCost == (Cost)null || bestCost.Coin.Value < supply.BaseCost.Coin.Value) { bestCost = supply.BaseCost; bestSupplies.Clear(); } bestSupplies.Add(supply); } } } if (bestSupplies.Count == 0) { if (buyableSupplies.Count() > 0) return buyableSupplies.ElementAt(random.Next(buyableSupplies.Count())); return null; } return bestSupplies[random.Next(bestSupplies.Count)]; } protected override Supply FindWorstCardForCost(IEnumerable buyableSupplies, Currency currency) { List worstSupplies = new List(); foreach (Supply supply in buyableSupplies) { // Only return ones we CAN gain if (currency != (Currency)null && !supply.CurrentCost.Equals(currency)) continue; if (ShouldBuy(supply)) continue; worstSupplies.Add(supply); } if (worstSupplies.Count == 0) return buyableSupplies.ElementAt(random.Next(buyableSupplies.Count())); return worstSupplies[random.Next(worstSupplies.Count)]; } protected override bool ShouldPlay(Card card) { if (!ShouldBuy(card.CardType)) return false; int previousTurnIndex = this._Game.TurnsTaken.Count - 2; Turn previousTurn = null; if (previousTurnIndex >= 0) previousTurn = this._Game.TurnsTaken[previousTurnIndex]; if (card.CardType == Cards.Base.TypeClass.Library) { // Don't play this if we're already at 7 cards (after playing it, of course) if (this.Hand.Count > 7) return false; } else if (card.CardType == Cards.Base.TypeClass.Mine) { // Check to see if there are any Treasure cards in the Supply that are better than at least one of the Treasure cards in my hand foreach (Card treasureCard in this.Hand[Category.Treasure]) { Cost treasureCardCost = _Game.Cost(treasureCard); if (this._Game.Table.Supplies.Values.Any(supply => supply.Count > 0 && (supply.Category & Category.Treasure) == Category.Treasure && supply.CurrentCost.Coin > treasureCardCost.Coin && supply.CurrentCost.Potion >= treasureCardCost.Potion)) return true; } return false; } else if (card.CardType == Cards.Base.TypeClass.Moneylender) { // Don't play if no Copper cards in hand if (this.Hand[Cards.Universal.TypeClass.Copper].Count == 0) return false; } else if (card.CardType == Cards.Base.TypeClass.ThroneRoom) { // Only play if there's at least 1 card in hand that we *can* play if (this.Hand[Category.Action].Any(c => ShouldBuy(c.CardType) && c.CardType != Cards.Base.TypeClass.ThroneRoom && c.CardType != Cards.Prosperity.TypeClass.KingsCourt)) return true; return false; } else if (card.CardType == Cards.Seaside.TypeClass.Island) { // Only play Island if we have another Victory-only card in hand if (this.Hand.Count(c => c.Category == Category.Victory) < 1) return false; } else if (card.CardType == Cards.Seaside.TypeClass.Outpost) { // Don't play if we're already in our 2nd turn if (previousTurn != null && previousTurn.Player == this) return false; } else if (card.CardType == Cards.Seaside.TypeClass.Tactician) { // Never play Tactician if there's one in play and we don't have anything to gain from playing another if (this.PreviousTableau[Cards.Seaside.TypeClass.Tactician].Count > 0 && (this.Hand.Count == 1 || (this.Currency.Coin + this.Hand[Category.Treasure].Sum(c => c.Benefit.Currency.Coin.Value)) > 4 || this.Hand[Category.Action].Count() > 2)) return false; } else if (card.CardType == Cards.Seaside.TypeClass.Smugglers) { Player playerToRight = this._Game.GetPlayerFromIndex(this, -1); Turn mostRecentTurn = this._Game.TurnsTaken.LastOrDefault(turn => turn.Player == playerToRight); if (mostRecentTurn == null) return false; // Only play Smugglers if the player to our right gained a card costing at least 2 (base price) if (mostRecentTurn.CardsGained.Any(c => c.BaseCost.Coin >= 2 && c.BaseCost.Potion == 0)) return true; return false; } // Yes, the AI should be smart enough to know exactly how many Copper cards are in its own discard pile else if (card.CardType == Cards.Prosperity.TypeClass.CountingHouse) { if (this.DiscardPile.LookThrough(c => c.CardType == Cards.Universal.TypeClass.Copper).Count == 0) return false; } else if (card.CardType == Cards.Prosperity.TypeClass.KingsCourt) { // Only play if there's at least 1 card in hand that we *can* play if (this.Hand[Category.Action].Any(c => ShouldBuy(c.CardType) && c.CardType != Cards.Base.TypeClass.ThroneRoom && c.CardType != Cards.Prosperity.TypeClass.KingsCourt)) return true; return false; } else if (card.CardType == Cards.Prosperity.TypeClass.Mint) { foreach (Card treasureCard in this.Hand[Category.Treasure]) { // We don't care about copying Copper cards if (treasureCard.CardType == Cards.Universal.TypeClass.Copper) continue; if (this._Game.Table.Supplies.ContainsKey(treasureCard) && this._Game.Table.Supplies[treasureCard].Count > 0) return true; } return false; } else if (card.CardType == Cards.Prosperity.TypeClass.Watchtower) { // Don't play this if we're already at 6 cards (after playing it, of course) if (this.Hand.Count > 6) return false; } return true; } protected override IEnumerable FindBestCardsToDiscard(IEnumerable cards, int count) { // choose the worse card in hand in this order // 1) Tunnel // 2) positive victory points // 3) Curse // 4) Ruins // 5) cheapest card left IEnumerable cardsToDiscard = cards; CardCollection cardsLeftOver = new CardCollection(); cardsToDiscard = cards.Where(c => c.CardType == Cards.Hinterlands.TypeClass.Tunnel); if (cardsToDiscard.Count() >= count) return cardsToDiscard.Take(count); cardsToDiscard = cardsToDiscard.Concat(cards.Where(c => c.Category == Category.Victory)); if (cardsToDiscard.Count() >= count) return cardsToDiscard.Take(count); cardsToDiscard = cardsToDiscard.Concat(cards.Where(c => c.Category == Category.Curse)); if (cardsToDiscard.Count() >= count) return cardsToDiscard.Take(count); cardsToDiscard = cardsToDiscard.Concat(cards.Where(c => (c.Category & Category.Ruins) == Category.Ruins)); if (cardsToDiscard.Count() >= count) return cardsToDiscard.Take(count); cardsToDiscard = cardsToDiscard.Concat(FindBestCardsToTrash(cards.Except(cardsToDiscard), count - cardsToDiscard.Count())); return cardsToDiscard.Take(count); } protected override IEnumerable FindBestCardsToTrash(IEnumerable cards, int count) { return FindBestCardsToTrash(cards, count, false); } protected IEnumerable FindBestCardsToTrash(IEnumerable cards, int count, Boolean onlyReturnTrashables) { // choose the worse card in hand in this order // 1) curse // 2) any ruins // 3) Sea Hag if there are no curses left // 4) Loan if we have fewer than 3 Coppers left // 5) Copper if we've got a lot of better Treasure // 6) Fortress // (If onlyReturnTrashables is false): // 7) cheapest card left CardCollection cardsToTrash = new CardCollection(); cardsToTrash.AddRange(cards.Where(c => c.Category == Category.Curse).Take(count)); if (cardsToTrash.Count >= count) return cardsToTrash; cardsToTrash.AddRange(cards.Where(c => (c.Category & Category.Ruins) == Category.Ruins).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; if (this._Game.Table.Curse.Count <= 1) { cardsToTrash.AddRange(cards.Where(c => c.CardType == Cards.Seaside.TypeClass.SeaHag).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; } if (this.CountAll(this, c => (c.Category & Category.Treasure) == Category.Treasure && (c.Benefit.Currency.Coin > 1 || c.CardType == Cards.Prosperity.TypeClass.Bank)) >= this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper)) { cardsToTrash.AddRange(cards.Where(c => c.CardType == Cards.Universal.TypeClass.Copper).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; } if (this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper) < 3) { cardsToTrash.AddRange(cards.Where(c => c.CardType == Cards.Prosperity.TypeClass.Loan).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; } cardsToTrash.AddRange(cards.Where(c => c.CardType == Cards.DarkAges.TypeClass.Fortress).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; if (!onlyReturnTrashables) cardsToTrash.AddRange(cards.OrderBy(c => c.BaseCost).ThenBy(c => c.Name).Where(c => !cardsToTrash.Contains(c)).Take(count - cardsToTrash.Count)); return cardsToTrash; } protected override IEnumerable FindBestCards(IEnumerable cards, int count) { // Chose the most expensive cards CardCollection cardsToReturn = new CardCollection(); cardsToReturn.AddRange(cards.OrderByDescending(c => c.BaseCost).ThenBy(c => c.Name).Take(count)); return cardsToReturn; } protected virtual Boolean IsCardOKForMeToDiscard(Card card) { if ((card.Category & Category.Curse) == Category.Curse) return true; if ((card.Category & Category.Ruins) == Category.Ruins) return true; if ((card.Category & Category.Victory) == Category.Victory && (card.Category & Category.Action) != Category.Action && (card.Category & Category.Treasure) != Category.Treasure) return true; if (card.CardType == Cards.Universal.TypeClass.Copper) return true; if (card.CardType == Cards.Hinterlands.TypeClass.Tunnel) return true; if (card.CardType == Cards.DarkAges.TypeClass.Hovel) return true; return false; } protected override ChoiceResult Decide_Attacked(Choice choice, AttackedEventArgs aea, IEnumerable cardsToReveal) { // Always reveal my Moat if the attack hasn't been cancelled yet if (!aea.Cancelled && cardsToReveal.Contains(Cards.Base.TypeClass.Moat)) return new ChoiceResult(new CardCollection() { aea.Revealable[Cards.Base.TypeClass.Moat].Card }); // Always reveal my Secret Chamber if it hasn't been revealed yet if ((_LastReactedCard == null || _LastReactedCard != choice.CardTriggers[0]) && cardsToReveal.Contains(Cards.Intrigue.TypeClass.SecretChamber) && !aea.HandledBy.Contains(Cards.Intrigue.TypeClass.SecretChamber)) return new ChoiceResult(new CardCollection() { aea.Revealable[Cards.Intrigue.TypeClass.SecretChamber].Card }); // Always reveal my Horse Traders if I can if (cardsToReveal.Contains(Cards.Cornucopia.TypeClass.HorseTraders)) return new ChoiceResult(new CardCollection() { aea.Revealable[Cards.Cornucopia.TypeClass.HorseTraders].Card }); // Always reveal my Horse Traders if I can if (cardsToReveal.Contains(Cards.DarkAges.TypeClass.Beggar)) { // Don't reveal Beggar for these Attacks -- Also, keep Beggar around if Young Witch is played and Beggar is the Bane card if (aea.AttackCard.CardType != Cards.Base.TypeClass.Thief && aea.AttackCard.CardType != Cards.Seaside.TypeClass.PirateShip && aea.AttackCard.CardType != Cards.Hinterlands.TypeClass.NobleBrigand && (aea.AttackCard.CardType != Cards.Cornucopia.TypeClass.YoungWitch || !this._Game.Table.Supplies[Cards.DarkAges.TypeClass.Beggar].Tokens.Any(t => t.GetType() != Cards.Cornucopia.TypeClass.BaneToken))) { return new ChoiceResult(new CardCollection() { aea.Revealable[Cards.DarkAges.TypeClass.Beggar].Card }); } } return new ChoiceResult(new CardCollection()); } protected override ChoiceResult Decide_CardBuy(Choice choice, CardBuyEventArgs cbea, IEnumerable cardTriggerTypes) { Type cardType = cardTriggerTypes.FirstOrDefault(t => t == Cards.Seaside.TypeClass.EmbargoToken || t == Cards.Prosperity.TypeClass.Hoard || t == Cards.Prosperity.TypeClass.Mint || t == Cards.Prosperity.TypeClass.Talisman || t == Cards.Hinterlands.TypeClass.Farmland || t == Cards.Hinterlands.TypeClass.Haggler || t == Cards.Hinterlands.TypeClass.NobleBrigand); if (cardType != null) return new ChoiceResult(new List() { cbea.Actions[cardType].Text }); return base.Decide_CardBuy(choice, cbea, cardTriggerTypes); } protected override ChoiceResult Decide_CardGain(Choice choice, CardGainEventArgs cgea, IEnumerable cardTriggerTypes) { // Always reveal & trash this if we don't have 2 or more in hand if (cardTriggerTypes.Contains(Cards.Hinterlands.TypeClass.FoolsGold)) { if (this.Hand[Cards.Hinterlands.TypeClass.FoolsGold].Count < 2) return new ChoiceResult(new List() { cgea.Actions[Cards.Hinterlands.TypeClass.FoolsGold].Text }); } // Always reveal Trader when Gaining a Curse or Copper -- Silver (or even nothing) is better anyway // This should happen before Watchtower -- we'll assume that gaining a Silver is better // than trashing the Curse or Copper if (cardTriggerTypes.Contains(Cards.Hinterlands.TypeClass.Trader)) { if (choice.CardTriggers[0].Category == Category.Curse || choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List() { cgea.Actions[Cards.Hinterlands.TypeClass.Trader].Text }); } // Always put card on top of your deck if (cardTriggerTypes.Contains(Cards.Prosperity.TypeClass.RoyalSeal)) { // Only put non-Curse & non-Victory-only cards on top of your deck if (choice.CardTriggers[0].Category != Category.Curse && choice.CardTriggers[0].Category != Category.Victory && choice.CardTriggers[0].CardType != Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List() { cgea.Actions[Cards.Prosperity.TypeClass.RoyalSeal].Text }); } // Always reveal for Curse & Copper cards from a Watchtower (to trash) if (cardTriggerTypes.Contains(Cards.Prosperity.TypeClass.Watchtower)) { if (choice.CardTriggers[0].Category != Category.Curse && choice.CardTriggers[0].Category != Category.Victory && choice.CardTriggers[0].CardType != Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List() { cgea.Actions[Cards.Prosperity.TypeClass.Watchtower].Text }); } // Eh... it's an OK card, I guess... don't really want too many of them // Especially if we have lots of Action cards and very few +2 or more Action cards if (cardTriggerTypes.Contains(Cards.Hinterlands.TypeClass.Duchess)) { double duchessMultipleActionCards = this.CountAll(this, c => (c.GroupMembership & Group.PlusMultipleActions) == Group.PlusMultipleActions, true, false); double duchessTotalActionCards = this.CountAll(this, c => (c.Category & Category.Action) == Category.Action, true, false); double duchessDuchesses = this.CountAll(this, c => c.CardType == Cards.Hinterlands.TypeClass.Duchess, true, false); double duchessTotalCards = this.CountAll(); if ((duchessTotalActionCards / duchessTotalCards) < 0.075 || (duchessDuchesses / duchessTotalActionCards) < 0.20 || (duchessMultipleActionCards / duchessTotalActionCards) > 0.33) return new ChoiceResult(new List() { cgea.Actions[Cards.Hinterlands.TypeClass.Duchess].Text }); } return new ChoiceResult(new List()); } protected override ChoiceResult Decide_CardsDiscard(Choice choice, CardsDiscardEventArgs cdea, IEnumerable cardTriggerTypes) { // Always put Treasury on my deck if I can if (cardTriggerTypes.Contains(Cards.Seaside.TypeClass.Treasury)) return new ChoiceResult(new List() { cdea.Actions[Cards.Seaside.TypeClass.Treasury].Text }); // Always put Alchemist on my deck if I can if (cardTriggerTypes.Contains(Cards.Alchemy.TypeClass.Alchemist)) return new ChoiceResult(new List() { cdea.Actions[Cards.Alchemy.TypeClass.Alchemist].Text }); // Only perform Herbalist if there's at least 1 non-Copper Treasure card in play if (cardTriggerTypes.Contains(Cards.Alchemy.TypeClass.Herbalist)) return new ChoiceResult(new List() { cdea.Actions[Cards.Alchemy.TypeClass.Herbalist].Text }); // Always reveal this when discarding if (cardTriggerTypes.Contains(Cards.Hinterlands.TypeClass.Tunnel)) return new ChoiceResult(new List() { cdea.Actions[Cards.Hinterlands.TypeClass.Tunnel].Text }); return new ChoiceResult(new List()); } protected override ChoiceResult Decide_CleaningUp(Choice choice, CleaningUpEventArgs cuea, IEnumerable cardTriggerTypes) { // Always choose a card with Scheme if I can (I should always be able to, yes?) if (cardTriggerTypes.Contains(Cards.Hinterlands.TypeClass.Scheme)) return new ChoiceResult(new List() { cuea.Actions[Cards.Hinterlands.TypeClass.Scheme].Text }); // Always put Walled Village on my deck if I can if (cardTriggerTypes.Contains(Cards.Promotional.TypeClass.WalledVillage)) return new ChoiceResult(new List() { cuea.Actions[Cards.Promotional.TypeClass.WalledVillage].Text }); return new ChoiceResult(new List()); } protected override ChoiceResult Decide_Trash(Choice choice, TrashEventArgs tea, IEnumerable cardTriggerTypes) { // Always reveal Market Square when we can if (cardTriggerTypes.Contains(Cards.DarkAges.TypeClass.MarketSquare)) return new ChoiceResult(new List() { tea.Actions[Cards.DarkAges.TypeClass.MarketSquare].Text }); // Resolve Sir Vander next -- not sure if any of these even matter if (cardTriggerTypes.Contains(Cards.DarkAges.TypeClass.SirVander)) return new ChoiceResult(new List() { tea.Actions[Cards.DarkAges.TypeClass.SirVander].Text }); // Resolve Feodum next -- not sure if any of these even matter if (cardTriggerTypes.Contains(Cards.DarkAges.TypeClass.Feodum)) return new ChoiceResult(new List() { tea.Actions[Cards.DarkAges.TypeClass.Feodum].Text }); return base.Decide_Trash(choice, tea, cardTriggerTypes); } protected override ChoiceResult Decide_RevealBane(Choice choice) { // Always reveal the Bane card if I can return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Altar(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Altar(choice); } } protected override ChoiceResult Decide_Ambassador(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // Always return as many copies as we can return new ChoiceResult(new List() { choice.Options[choice.Options.Count - 1] }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); default: return base.Decide_Ambassador(choice); } } protected override ChoiceResult Decide_Apothecary(Choice choice) { CardCollection apothCards = new CardCollection(choice.Cards); // Order them in roughly random order Utilities.Shuffler.Shuffle(apothCards); return new ChoiceResult(apothCards); } protected override ChoiceResult Decide_Apprentice(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_Armory(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Baron(Choice choice) { // Always discard an Estate if I can return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Bishop(Choice choice) { if (choice.Text.StartsWith("Trash a card.")) { // All of this logic is based on the assumption that all costs are standard. // Obviously, Bridge, Princess, Highway, and even Quarry can muck this up // Trash Curses first Card bishopBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Curse) == Category.Curse); // Useless Seahags & Familiars go next if (bishopBestCard == null && _Game.Table.Curse.Count < _Game.Players.Count) bishopBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Seaside.TypeClass.SeaHag || c.CardType == Cards.Alchemy.TypeClass.Familiar); // Ruins go next if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Ruins) == Category.Ruins); // Estates are usually a great choice as well if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Estate); // Rats are usually a great choice as well if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.DarkAges.TypeClass.Rats); // Fortress is sweet -- it comes right back into my hand if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.DarkAges.TypeClass.Fortress); // Trasher cards (since we really won't play them anyway) are also good // As are cards that are pretty useless in certain situations // e.g. Coppersmith, Counting House, Loan, or Spice Merchant with very little Copper available, // Treasure Map with plenty of gold already, Potion with no Supply piles costing potions left if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => (c.CardType == Cards.Base.TypeClass.Bureaucrat && this.GameProgress < 0.4) || (c.CardType == Cards.Base.TypeClass.Moneylender && this.CountAll(this, cC => cC.CardType == Cards.Universal.TypeClass.Copper, true, false) < 3) || c.CardType == Cards.Base.TypeClass.Remodel || (c.CardType == Cards.Intrigue.TypeClass.Coppersmith && this.CountAll(this, cC => cC.CardType == Cards.Universal.TypeClass.Copper, true, false) < 5) || (c.CardType == Cards.Intrigue.TypeClass.Ironworks && this.GameProgress < 0.4) || c.CardType == Cards.Intrigue.TypeClass.Masquerade || c.CardType == Cards.Intrigue.TypeClass.TradingPost || c.CardType == Cards.Intrigue.TypeClass.Upgrade || c.CardType == Cards.Seaside.TypeClass.Ambassador || c.CardType == Cards.Seaside.TypeClass.Lookout || c.CardType == Cards.Seaside.TypeClass.Salvager || (c.CardType == Cards.Seaside.TypeClass.TreasureMap && this.CountAll(this, cG => cG.CardType == Cards.Universal.TypeClass.Gold, true, false) > 2) || (c.CardType == Cards.Alchemy.TypeClass.Potion && !_Game.Table.Supplies.Any(kvp => kvp.Value.BaseCost.Potion.Value > 0 && kvp.Value.CanGain())) || (c.CardType == Cards.Prosperity.TypeClass.CountingHouse && this.CountAll(this, cC => cC.CardType == Cards.Universal.TypeClass.Copper, true, false) < 5) || c.CardType == Cards.Prosperity.TypeClass.Expand || c.CardType == Cards.Prosperity.TypeClass.Forge || (c.CardType == Cards.Prosperity.TypeClass.Loan && this.CountAll(this, cC => cC.CardType == Cards.Universal.TypeClass.Copper, true, false) < 3) || c.CardType == Cards.Cornucopia.TypeClass.Remake || c.CardType == Cards.Hinterlands.TypeClass.Develop || (c.CardType == Cards.Hinterlands.TypeClass.SpiceMerchant && this.CountAll(this, cC => cC.CardType == Cards.Universal.TypeClass.Copper || cC.CardType == Cards.Prosperity.TypeClass.Loan, true, false) < 4) || (c.CardType == Cards.DarkAges.TypeClass.HuntingGrounds && this.GameProgress < 0.4) || (c.CardType == Cards.DarkAges.TypeClass.SirVander && this.GameProgress < 0.3) || (c.CardType == Cards.DarkAges.TypeClass.Armory && this.GameProgress < 0.4) ); // Copper is a distant 8th if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Copper); // If a suitable one's STILL not been found, allow Peddler, well, because getting an extra 4 VPs off Peddler is *AMAZING* if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Prosperity.TypeClass.Peddler); // Otherwise, choose a non-Victory card to trash if (bishopBestCard == null) { IEnumerable bishCards = this.FindBestCardsToTrash(choice.Cards.Where(c => (c.Category & Category.Victory) != Category.Victory), 1); if (bishCards.Count() > 0) bishopBestCard = bishCards.ElementAt(0); } // Duchies or Dukes are usually an OK choice if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Duchy || c.CardType == Cards.Intrigue.TypeClass.Duke ); // OK, last chance... just PICK one! if (bishopBestCard == null) { IEnumerable bishCards = this.FindBestCardsToTrash(choice.Cards, 1); if (bishCards.Count() > 0) bishopBestCard = bishCards.ElementAt(0); } if (bishopBestCard != null) return new ChoiceResult(new CardCollection() { bishopBestCard }); return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 1))); } else // Optionally trash a card -- other players { // Always choose to trash a Curse from Bishop if I have one if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Curse) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => c.CardType == Cards.Universal.TypeClass.Curse) }); // Always choose to trash a Ruins from Bishop if I have one else if (choice.Cards.Count(c => (c.Category & Category.Ruins) == Category.Ruins) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => (c.Category & Category.Ruins) == Category.Ruins) }); else return new ChoiceResult(new CardCollection()); } } protected override ChoiceResult Decide_BorderVillage(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Bureaucrat(Choice choice) { return new ChoiceResult(new CardCollection() { choice.Cards.ElementAt(random.Next(choice.Cards.Count())) }); } protected override ChoiceResult Decide_Cartographer(Choice choice) { if (choice.Text.StartsWith("Choose cards to discard")) { // Grab all cards that we don't really care about return new ChoiceResult(new CardCollection(choice.Cards.Where(c => IsCardOKForMeToDiscard(c)))); } else { CardCollection cartCards = new CardCollection(choice.Cards); // Order them in roughly random order Utilities.Shuffler.Shuffle(cartCards); return new ChoiceResult(cartCards); } } protected override ChoiceResult Decide_Cellar(Choice choice) { CardCollection cellarCards = new CardCollection(); // TODO -- the AI makes slightly bad decisions when it comes to Cellar -- Fix me! cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.Base.TypeClass.Cellar)); cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Curse)); cellarCards.AddRange(choice.Cards.Where(c => (c.Category & Category.Ruins) == Category.Ruins)); cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.Hinterlands.TypeClass.Tunnel)); cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.DarkAges.TypeClass.Hovel)); cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.DarkAges.TypeClass.OvergrownEstate)); cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.DarkAges.TypeClass.Rats)); cellarCards.AddRange(choice.Cards.Where(c => (c.Category == Category.Victory && c.CardType != Cards.Universal.TypeClass.Estate && c.CardType != Cards.Universal.TypeClass.Province))); if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Estate) > 0) cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Estate).Take(choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Estate) - choice.Cards.Count(c => c.CardType == Cards.Intrigue.TypeClass.Baron))); if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Province) > 0) cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Province).Take(choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Province) - choice.Cards.Count(c => c.CardType == Cards.Cornucopia.TypeClass.Tournament))); if (this.GameProgress < 0.63) cellarCards.AddRange(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Copper)); return new ChoiceResult(cellarCards); } protected override ChoiceResult Decide_Chancellor(Choice choice) { // Never put deck into discard pile later in the game -- we don't want those Victory cards back in our deck sooner if (this.GameProgress < 0.30) return new ChoiceResult(new List() { choice.Options[1] }); return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Chapel(Choice choice) { CardCollection chapelToTrash = new CardCollection(); // Always choose to trash all Curses chapelToTrash.AddRange(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Curse).Take(4)); // Always choose to trash all Ruins chapelToTrash.AddRange(choice.Cards.Where(c => (c.Category & Category.Ruins) == Category.Ruins).Take(4 - chapelToTrash.Count)); return new ChoiceResult(chapelToTrash); } protected override ChoiceResult Decide_Count(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: String option = String.Empty; // First choice if (choice.Options[0] == "Discard 2 cards") { // This is it for now -- very complicated decision-making to decide if we have 2 cards that we're willing to discard // The assumption here is that we should want to play everything that's of value in our hand (which is very bad, in general) if (this.Hand.Count(c => c.CardType == Cards.Universal.TypeClass.Curse || c.Category == Category.Victory || (c.Category & Category.Ruins) == Category.Ruins || ((c.Category & Category.Action) == Category.Action && this.Actions <= 0) || (c.CardType == Cards.Seaside.TypeClass.SeaHag && this._Game.Table.Curse.Count < this._Game.Players.Count) || (c.CardType == Cards.Prosperity.TypeClass.CountingHouse && this.DiscardPile.Count(dc => dc.CardType == Cards.Universal.TypeClass.Copper) == 0) || (c.CardType == Cards.Base.TypeClass.Moneylender && this.Hand.Count(hc => hc.CardType == Cards.Universal.TypeClass.Copper) == 0) || ((c.CardType == Cards.Base.TypeClass.ThroneRoom || c.CardType == Cards.Prosperity.TypeClass.KingsCourt || c.CardType == Cards.DarkAges.TypeClass.Procession) && this.Hand.Count(hc => (hc.Category & Category.Action) == Category.Action && hc.CardType != Cards.Base.TypeClass.ThroneRoom && hc.CardType != Cards.Prosperity.TypeClass.KingsCourt && hc.CardType != Cards.DarkAges.TypeClass.Procession) == 0) || (c.CardType == Cards.Seaside.TypeClass.TreasureMap && this.Hand.Count(hc => hc.CardType == Cards.Seaside.TypeClass.TreasureMap) < 2) ) > 2) option = choice.Options[0]; else option = choice.Options[1]; } // Second choice else { if (this.GameProgress <= 0.4 && this._Game.Table.Duchy.CanGain()) option = choice.Options[2]; else if (( // Trash our hand if we have more than 3 of only Curse/Copper/Ruins/Rats cards this.Hand.Count == this.Hand[c => c.CardType == Cards.Universal.TypeClass.Copper || c.CardType == Cards.Universal.TypeClass.Curse || (c.Category & Category.Ruins) == Category.Ruins || c.CardType == Cards.DarkAges.TypeClass.Rats].Count) && this.Hand.Count > 3 ) option = choice.Options[1]; else option = choice.Options[0]; } if (option != String.Empty) return new ChoiceResult(new List() { option }); break; case ChoiceType.Cards: if (choice.Text == "Discard 2 cards.") { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 2))); } else if (choice.Text == "Choose a card to put back on your deck") { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 1))); } break; } return base.Decide_Count(choice); } protected override ChoiceResult Decide_Counterfeit(Choice choice) { // This is a first stab at a decision-making strategy for Counterfeit // It's not entirely efficient or good at what it does, but it's at least better than picking a random Treasure from time to time // There are some amazing power moves like a late-game Counterfeit'ed Platinum or Bank that can be pretty amazingly devastating // but more analysis needs to be done to find out the exact timing for stuff like that. A decision tree to know how much buying // power we currently have and how much we need to do certain things would be really nice & help this choice out immensely Card counterfeitingCard = null; // Spoils are first, since they're super, super awesome w/ Counterfeit counterfeitingCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.DarkAges.TypeClass.Spoils); // If we've got a few Coppers left, let's do one of those if (counterfeitingCard == null && this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false) > 4) counterfeitingCard = choice.Cards.FirstOrDefault(card => card.CardType == Cards.Universal.TypeClass.Copper); // If we've got a Loan and not many Coppers left, we don't want to keep the Loan around anyway if (counterfeitingCard == null && this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false) < 4) counterfeitingCard = choice.Cards.FirstOrDefault(card => card.CardType == Cards.Prosperity.TypeClass.Loan); // If the game progress is at least 1/2-way & we've got at least 3 buys, then let's use a Quarry if (counterfeitingCard == null && this.GameProgress < 0.50 && this.Buys > 2) counterfeitingCard = choice.Cards.FirstOrDefault(card => card.CardType == Cards.Prosperity.TypeClass.Quarry); // If the game progress is at least 1/3-way & we've got at least 6 Silvers or at least 4 cards better than a Silver, let's use a Silver if (counterfeitingCard == null && this.GameProgress < 0.66 && (this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Silver, true, false) >= 6 || this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Gold || c.CardType == Cards.Intrigue.TypeClass.Harem || c.CardType == Cards.Alchemy.TypeClass.PhilosophersStone || c.CardType == Cards.Prosperity.TypeClass.Bank || c.CardType == Cards.Prosperity.TypeClass.Platinum || c.CardType == Cards.Prosperity.TypeClass.RoyalSeal || c.CardType == Cards.Prosperity.TypeClass.Venture || c.CardType == Cards.Hinterlands.TypeClass.Cache, true, false) >= 6)) counterfeitingCard = choice.Cards.FirstOrDefault(card => card.CardType == Cards.Universal.TypeClass.Silver); // If we've got an Ill-Gotten Gains, use that (it's main use is the Gain, anyway) if (counterfeitingCard == null) counterfeitingCard = choice.Cards.FirstOrDefault(card => card.CardType == Cards.Hinterlands.TypeClass.IllGottenGains); if (counterfeitingCard != null) return new ChoiceResult(new CardCollection() { counterfeitingCard }); // Don't play anything return new ChoiceResult(new CardCollection()); } protected override ChoiceResult Decide_CountingHouse(Choice choice) { // Always grab all of the coppers return new ChoiceResult(new List() { choice.Options[choice.Options.Count - 1] }); } protected override ChoiceResult Decide_Contraband(Choice choice) { // Let's guess how many coins the player has left. // We'll assume roughly 2 coins per card left in his hand (scale that slightly based on how early in the game it is) double multiplier = 2.0; if (this.GameProgress > 0.85) multiplier = 1.75; else if (this.GameProgress > 0.65) multiplier = 1.85; int remainingCoins = Math.Max(0, (int)(0.5 + multiplier * choice.PlayerSource.Hand.Count + 3 * Utilities.Gaussian.NextGaussian())); Supply supply = null; while (supply == null) { supply = FindBestCardForCost(choice.Supplies.Values.Where( s => s.Count > 0 && !s.Tokens.Any(t => t.GetType() == Cards.Prosperity.TypeClass.ContrabandToken)), choice.PlayerSource.Currency + new Currencies.Coin(remainingCoins), false); remainingCoins++; } return new ChoiceResult(supply); } protected override ChoiceResult Decide_Courtyard(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult Decide_Cultist(Choice choice) { // Always return "Yes" return new ChoiceResult(new List() { choice.Options[0] }); // Yes } protected override ChoiceResult Decide_DameAnna(Choice choice) { if (choice.Text == "Choose up to 2 cards to trash") { CardCollection dameAnnaToTrash = new CardCollection(); // Always choose to trash all Curses dameAnnaToTrash.AddRange(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Curse).Take(2)); // Always choose to trash all Ruins dameAnnaToTrash.AddRange(choice.Cards.Where(c => (c.Category & Category.Ruins) == Category.Ruins).Take(2 - dameAnnaToTrash.Count)); return new ChoiceResult(dameAnnaToTrash); } else if (choice.Text == "Choose a card to trash") return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); else return base.Decide_Rogue(choice); } protected override ChoiceResult Decide_DameJosephine(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_DameMolly(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_DameNatalie(Choice choice) { if (choice.Text.StartsWith("You may gain a card")) { Supply supply = FindBestCardForCost(choice.Supplies.Values, null, false); if (supply.CardType == Cards.Universal.TypeClass.Curse || supply.CardType == Cards.Universal.TypeClass.Copper || (supply.Category & Category.Ruins) == Category.Ruins) supply = null; return new ChoiceResult(supply); } else if (choice.Text == "Choose a card to trash") return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); else return base.Decide_Rogue(choice); } protected override ChoiceResult Decide_DameSylvia(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_Develop(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Develop(choice); } } protected override ChoiceResult Decide_Duchess(Choice choice) { if (this.IsCardOKForMeToDiscard(choice.CardTriggers[0])) return new ChoiceResult(new List() { choice.Options[0] }); else return new ChoiceResult(new List() { choice.Options[1] }); } protected override ChoiceResult Decide_Embargo(Choice choice) { List embargoAbleSupplies = new List(); foreach (Supply supply in choice.Supplies.Values.Where(s => s.SupplyCardType != Cards.Universal.TypeClass.Curse)) { if (!ShouldBuy(supply)) embargoAbleSupplies.Add(supply); } if (embargoAbleSupplies.Count == 0) embargoAbleSupplies.Add(choice.Supplies[Cards.Universal.TypeClass.Province]); return new ChoiceResult(embargoAbleSupplies[random.Next(embargoAbleSupplies.Count)]); } protected override ChoiceResult Decide_Embassy(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 3))); } protected override ChoiceResult Decide_Envoy(Choice choice) { // Find most-expensive non-Victory card to discard // Focus only on Treasure cards if there are no Actions remaining Card cardEnvoy = null; foreach (Card card in choice.Cards) { if (((card.Category & Category.Treasure) == Category.Treasure || (_Game.TurnsTaken.Last().Player.Actions > 0 && (card.Category & Category.Action) == Category.Action)) && (cardEnvoy == null || (card.BaseCost.Coin.Value + 2.5 * card.BaseCost.Potion.Value) > (cardEnvoy.BaseCost.Coin.Value + 2.5 * cardEnvoy.BaseCost.Potion.Value))) cardEnvoy = card; } if (cardEnvoy != null) return new ChoiceResult(new CardCollection() { cardEnvoy }); return base.Decide_Envoy(choice); } protected override ChoiceResult Decide_Expand(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Expand(choice); } } protected override ChoiceResult Decide_Explorer(Choice choice) { // Always reveal a Province if we can return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Farmland(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: // Always trash Curses if we can Card farmlandBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Curse) == Category.Curse); // Trashing a 9-cost non-Victory card for a Colony later in the game seems like a good idea if (farmlandBestCard == null && this.GameProgress < 0.5 && _Game.Table.Supplies.ContainsKey(Cards.Prosperity.TypeClass.Colony) && _Game.Table.Supplies[Cards.Prosperity.TypeClass.Colony].CanGain()) farmlandBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Victory) != Category.Victory && c.BaseCost == new Cost(9)); // Trashing a 6-cost non-Victory card for a Province later in the game seems like a good idea if (farmlandBestCard == null && this.GameProgress < 0.5 && _Game.Table[Cards.Universal.TypeClass.Province].CanGain()) farmlandBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Victory) != Category.Victory && c.BaseCost == new Cost(6)); // Trashing a 9-cost Victory card for a Colony seems like a good idea if (farmlandBestCard == null && _Game.Table.Supplies.ContainsKey(Cards.Prosperity.TypeClass.Colony) && _Game.Table.Supplies[Cards.Prosperity.TypeClass.Colony].CanGain()) farmlandBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Victory) == Category.Victory && c.BaseCost == new Cost(9)); // Trashing a 6-cost Victory card for a Province seems like a good idea if (farmlandBestCard == null && _Game.Table[Cards.Universal.TypeClass.Province].CanGain()) farmlandBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Victory) == Category.Victory && c.BaseCost == new Cost(6)); // Trash Copper later in the game -- they just suck if (farmlandBestCard == null && this.GameProgress < 0.65) farmlandBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Copper); if (farmlandBestCard != null) return new ChoiceResult(new CardCollection() { farmlandBestCard }); return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Farmland(choice); } } protected override ChoiceResult Decide_Feast(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Followers(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult Decide_Forge(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: // Only trash Curses & Ruins CardCollection forgeToTrash = new CardCollection(); // Always choose to trash all Curses forgeToTrash.AddRange(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Curse)); // Always choose to trash all Ruins forgeToTrash.AddRange(choice.Cards.Where(c => (c.Category & Category.Ruins) == Category.Ruins)); return new ChoiceResult(forgeToTrash); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Forge(choice); } } protected override ChoiceResult Decide_GhostShip(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult Decide_Golem(Choice choice) { // Just choose one at random. return new ChoiceResult(new CardCollection() { choice.Cards.ElementAt(random.Next(choice.Cards.Count())) }); } protected override ChoiceResult Decide_Goons(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult Decide_Governor(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: if (this.Hand[c => c.CardType == Cards.Universal.TypeClass.Curse || (c.Category & Category.Ruins) == Category.Ruins].Count > 0) return new ChoiceResult(new List() { choice.Options[2] }); // Trash a card return new ChoiceResult(new List() { choice.Options[0] }); // +1(+3) Cards case ChoiceType.Cards: // Only ever trash Curses or Ruins if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Curse || (c.Category & Category.Ruins) == Category.Ruins) > 0) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); else return new ChoiceResult(new CardCollection()); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Governor(choice); } } protected override ChoiceResult Decide_Graverobber(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: List availableCosts = new List() { new Cost(3), new Cost(4), new Cost(5), new Cost(6) }; // If it's later in the game and there are Victory cards in the Trash between 3 & 6, then gain a Victory card if (this.GameProgress < 0.4 && this._Game.Table.Trash.Count(c => availableCosts.Any(cost => cost == this._Game.Cost(c)) && (c.Category & Category.Victory) == Category.Victory) > 0) return new ChoiceResult(new List() { choice.Options[0] }); // Gain a card from the trash // Choose to trash a Ruins from Graverobber if I have one else if (this.Hand.Count(c => (c.Category & Category.Ruins) == Category.Ruins) > 0) return new ChoiceResult(new List() { choice.Options[1] }); // Trash an Action card else return new ChoiceResult(new List() { choice.Options[0] }); // Gain a card from the trash case ChoiceType.Cards: if (choice.Text == "Gain a card from the trash") { // If it's later in the game and there are Victory cards in the Trash between 3 & 6, then gain the best Victory card if (this.GameProgress < 0.4 && choice.Cards.Count(c => (c.Category & Category.Victory) == Category.Victory) > 0) return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards.Where(c => (c.Category & Category.Victory) == Category.Victory), 1))); else return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards, 1))); } else // "Choose an Action card to trash" { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Graverobber(choice); } } protected override ChoiceResult Decide_Haggler(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Hamlet(Choice choice) { if (choice.Text == "You may discard a card for +1 Action.") { // Tunnel is always a great bet Card hamletABestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Hinterlands.TypeClass.Tunnel); // No? How about others... if (hamletABestCard == null) { int actionTerminationsLeft = this.Hand.Count(c => this.ShouldPlay(c) && (c.Category & Category.Action) == Category.Action && (c.GroupMembership & Group.PlusAction) != Group.PlusAction); int actionSplitsLeft = this.Hand.Count(c => this.ShouldPlay(c) && (c.Category & Category.Action) == Category.Action && (c.GroupMembership & Group.PlusMultipleActions) == Group.PlusMultipleActions); int actionChainsLeft = this.Hand.Count(c => this.ShouldPlay(c) && (c.Category & Category.Action) == Category.Action && (c.GroupMembership & Group.PlusAction) == Group.PlusAction) - actionSplitsLeft; // Adjust this number a bit -- TR/KC can make an ActionChain an ActionSplit (end result of an extra 1 or 2 Actions left) actionSplitsLeft += Math.Min(this.Hand[Cards.Base.TypeClass.ThroneRoom].Count, actionChainsLeft); actionSplitsLeft += Math.Min(2 * this.Hand[Cards.Prosperity.TypeClass.KingsCourt].Count, actionChainsLeft); // Only the first Crossroads counts for action splitting, *AND HOW* if it does! if (this.Hand[Cards.Hinterlands.TypeClass.Crossroads].Count > 0) { actionSplitsLeft -= Math.Max(0, this.Hand[Cards.Hinterlands.TypeClass.Crossroads].Count - 1); if (this.CurrentTurn.CardsPlayed.Any(c => c.CardType == Cards.Hinterlands.TypeClass.Crossroads)) actionSplitsLeft--; else actionSplitsLeft++; } int actionPlayDeficit = this.Actions - actionTerminationsLeft; // If we've got a deficit of Actions left to play our Action cards and we've got a candidate card to discard, then let's go for it! if (actionPlayDeficit < 0) { hamletABestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Curse) == Category.Curse || (c.Category & Category.Ruins) == Category.Ruins || ((c.Category & Category.Victory) == Category.Victory && (c.Category & Category.Action) != Category.Action && (c.Category & Category.Treasure) != Category.Treasure)); // Open it up to Coppers now if (hamletABestCard == null) hamletABestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Copper); } } if (hamletABestCard != null) return new ChoiceResult(new CardCollection() { hamletABestCard }); return new ChoiceResult(new CardCollection()); } else { // Tunnel is always a great bet Card hamletBBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Hinterlands.TypeClass.Tunnel); // Fairly simple analysis on the +1 Buy front if (this.Buys < 2 && this.Hand[Category.Treasure].Sum(c => c.Benefit.Currency.Coin.Value) + this.Currency.Coin.Value > 6) hamletBBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Curse) == Category.Curse || (c.Category & Category.Ruins) == Category.Ruins || ((c.Category & Category.Victory) == Category.Victory && (c.Category & Category.Action) != Category.Action && (c.Category & Category.Treasure) != Category.Treasure)); if (hamletBBestCard != null) return new ChoiceResult(new CardCollection() { hamletBBestCard }); return new ChoiceResult(new CardCollection()); } } protected override ChoiceResult Decide_Haven(Choice choice) { Card havenBestCard = null; if (this.Currency.Coin > 4) { havenBestCard = choice.Cards.Where(c => (c.Category & Category.Action) == Category.Action).OrderByDescending(c => c.BaseCost.Coin.Value + 2.5 * c.BaseCost.Potion.Value).FirstOrDefault(); if (havenBestCard == null) { // If there are none, pick a random non-Treasure card instead IEnumerable havenNonTreasures = choice.Cards.Where(c => (c.Category & Category.Treasure) != Category.Treasure); return new ChoiceResult(new CardCollection() { havenNonTreasures.ElementAt(random.Next(havenNonTreasures.Count())) }); } } else { // We don't have a lot of gold, so choose our biggest coin IEnumerable havenTreasures = choice.Cards.Where(c => (c.Category & Category.Treasure) == Category.Treasure); // If there are no Treasures, try to grab Action cards instead if (havenTreasures.Count() == 0) havenTreasures = choice.Cards.Where(c => (c.Category & Category.Action) == Category.Action); // Just pick any old card if (havenTreasures.Count() == 0) havenTreasures = choice.Cards; return new ChoiceResult(new CardCollection() { havenTreasures.ElementAt(random.Next(havenTreasures.Count())) }); } // Just pick a random card if we still haven't found a decent one return new ChoiceResult(new CardCollection() { choice.Cards.ElementAt(random.Next(choice.Cards.Count())) }); } protected override ChoiceResult Decide_Herbalist(Choice choice) { // Always choose the Treasure card that costs the most to put on top, except if it's Copper (F Copper) Card bestCard = choice.Cards. OrderByDescending(card => (card.Category & Category.Prize) == Category.Prize ? new Cost(7) : card.BaseCost). FirstOrDefault(); if (bestCard != null && bestCard.CardType != Cards.Universal.TypeClass.Copper) return new ChoiceResult(new CardCollection() { bestCard }); return Decide_Herbalist(choice); } protected override ChoiceResult Decide_Hermit(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: // Always choose to trash a Curse from Hermit if possible if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Curse) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => c.CardType == Cards.Universal.TypeClass.Curse) }); // Always choose to trash a Ruins from Hermit if possible else if (choice.Cards.Count(c => (c.Category & Category.Ruins) == Category.Ruins) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => (c.Category & Category.Ruins) == Category.Ruins) }); else return new ChoiceResult(new CardCollection()); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Hermit(choice); } } protected override ChoiceResult Decide_HornOfPlenty(Choice choice) { // If it's early on, never gain a Victory card (since that trashes the Horn of Plenty) // Also, only do it about 2/5 the time after that if (this.GameProgress > 0.35 || random.Next(5) <= 1) return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.Where(supply => (supply.Category & Category.Victory) != Category.Victory), null, false)); else return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_HorseTraders(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 2))); } protected override ChoiceResult Decide_HuntingGrounds(Choice choice) { String hgChoice = choice.Options[0]; // Only choose Estates if there are no Duchies or if we're at the end game, there are at least 3 Estates, and we've got at least 1 Gardens/Silk Road if (this._Game.Table.Duchy.Count == 0) hgChoice = choice.Options[1]; if (this._Game.IsEndgameTriggered && this._Game.Table.Estate.Count >= 3 && this.CountAll(this, c => c.CardType == Cards.Base.TypeClass.Gardens || c.CardType == Cards.Hinterlands.TypeClass.SilkRoad) > 0) hgChoice = choice.Options[1]; return new ChoiceResult(new List() { hgChoice }); } protected override ChoiceResult Decide_IllGottenGains(Choice choice) { // Always take the Copper -- more money = better, right? return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Inn(Choice choice) { if (choice.Text.StartsWith("Discard 2 cards")) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 2))); } else { // Always select ALL Action cards we want to play return new ChoiceResult(new CardCollection(choice.Cards.Where(c => this.ShouldPlay(c)))); } } protected override ChoiceResult Decide_Ironmonger(Choice choice) { if (this.IsCardOKForMeToDiscard(choice.CardTriggers[0])) return new ChoiceResult(new List() { choice.Options[0] }); // Discard else return new ChoiceResult(new List() { choice.Options[1] }); // Put back } protected override ChoiceResult Decide_Ironworks(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Island(Choice choice) { Card islandBestCard = choice.Cards.Where(c => c.Category == Category.Victory || c.Category == Category.Curse).OrderByDescending(c => c.BaseCost.Coin.Value + 2.5 * c.BaseCost.Potion.Value).FirstOrDefault(c => true); if (islandBestCard != null) return new ChoiceResult(new CardCollection() { islandBestCard }); return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult Decide_JackOfAllTrades(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: if (choice.CardTriggers[0].Category == Category.Victory || choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List() { choice.Options[0] }); // Discard else if (choice.CardTriggers[0].Category == Category.Curse) { // Put a Curse or Ruins back if we can draw it and trash it (and we don't have a Curse or Ruins already in hand) if (this.Hand.Count < 5 && this.Hand[Category.Curse].Count == 0 && this.Hand[Category.Ruins].Count == 0) return new ChoiceResult(new List() { choice.Options[1] }); // Put it back else return new ChoiceResult(new List() { choice.Options[1] }); // Discard } else return new ChoiceResult(new List() { choice.Options[1] }); // Put it back case ChoiceType.Cards: // Only ever trash Curses, Ruins, and SeaHags if there are no Curses left Card joatCurse = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Curse || (c.Category & Category.Ruins) == Category.Ruins || (c.CardType == Cards.Seaside.TypeClass.SeaHag && !_Game.Table[Cards.Universal.TypeClass.Curse].CanGain())); if (joatCurse != null) return new ChoiceResult(new CardCollection() { joatCurse }); return new ChoiceResult(new CardCollection()); default: return base.Decide_JackOfAllTrades(choice); } } protected override ChoiceResult Decide_Jester(Choice choice) { Type cardType = choice.CardTriggers[0].CardType; // HUGE list of cards & criteria for which player gets the card if (cardType == Cards.Universal.TypeClass.Curse || cardType == Cards.Universal.TypeClass.Copper || (choice.CardTriggers[0].Category & Category.Ruins) == Category.Ruins || (cardType == Cards.Base.TypeClass.Chapel && this.CountAll(this, c => c.CardType == Cards.Base.TypeClass.Chapel, true, false) > 2) || (cardType == Cards.Base.TypeClass.Mine && this.CountAll(this, c => c.CardType == Cards.Base.TypeClass.Mine, true, false) > 2) || (cardType == Cards.Base.TypeClass.Moneylender && this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false) < 4) || cardType == Cards.Base.TypeClass.Remodel || (cardType == Cards.Base.TypeClass.Workshop && _Game.Table.Supplies.Count(kvp => kvp.Value.BaseCost == new Cost(4)) < 2) || (cardType == Cards.Intrigue.TypeClass.Coppersmith && this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false) < 4) || (cardType == Cards.Intrigue.TypeClass.Ironworks && _Game.Table.Supplies.Count(kvp => kvp.Value.BaseCost == new Cost(4)) < 3) || cardType == Cards.Intrigue.TypeClass.Masquerade || cardType == Cards.Intrigue.TypeClass.TradingPost || cardType == Cards.Seaside.TypeClass.Ambassador || cardType == Cards.Seaside.TypeClass.Lookout || cardType == Cards.Seaside.TypeClass.Salvager || (cardType == Cards.Seaside.TypeClass.SeaHag && !_Game.Table[Cards.Universal.TypeClass.Curse].CanGain()) || cardType == Cards.Seaside.TypeClass.TreasureMap || (cardType == Cards.Prosperity.TypeClass.Contraband && this.CountAll(this, c => c.CardType == Cards.Prosperity.TypeClass.Contraband, true, false) > 3) || (cardType == Cards.Prosperity.TypeClass.CountingHouse && this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false) < 4) || cardType == Cards.Prosperity.TypeClass.Expand || cardType == Cards.Prosperity.TypeClass.Forge || (cardType == Cards.Prosperity.TypeClass.Mint && this.CountAll(this, c => c.CardType == Cards.Prosperity.TypeClass.Mint, true, false) > 2) || (cardType == Cards.Prosperity.TypeClass.Talisman && (_Game.Table.Supplies.Count(kvp => kvp.Value.BaseCost == new Cost(4)) < 3 || this.CountAll(this, c => c.CardType == Cards.Prosperity.TypeClass.Talisman, true, false) > 2)) || cardType == Cards.Prosperity.TypeClass.TradeRoute || (cardType == Cards.Cornucopia.TypeClass.HornOfPlenty && this.CountAll(this, c => c.CardType == Cards.Cornucopia.TypeClass.HornOfPlenty, true, false) > 3) || cardType == Cards.Cornucopia.TypeClass.Remake || (cardType == Cards.Cornucopia.TypeClass.YoungWitch && !_Game.Table[Cards.Universal.TypeClass.Curse].CanGain()) || cardType == Cards.Hinterlands.TypeClass.Develop || cardType == Cards.DarkAges.TypeClass.Procession || cardType == Cards.DarkAges.TypeClass.Rats || cardType == Cards.DarkAges.TypeClass.Rebuild) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_JunkDealer(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_KingsCourt(Choice choice) { Card bestCard = this.FindBestCardToPlay(choice.Cards.Where(c => c.CardType != Cards.Base.TypeClass.Chapel && c.CardType != Cards.Base.TypeClass.Library && c.CardType != Cards.Base.TypeClass.Remodel && c.CardType != Cards.Intrigue.TypeClass.SecretChamber && c.CardType != Cards.Intrigue.TypeClass.Upgrade && c.CardType != Cards.Seaside.TypeClass.Island && c.CardType != Cards.Seaside.TypeClass.Lookout && c.CardType != Cards.Seaside.TypeClass.Outpost && c.CardType != Cards.Seaside.TypeClass.Salvager && c.CardType != Cards.Seaside.TypeClass.Tactician && c.CardType != Cards.Seaside.TypeClass.TreasureMap && c.CardType != Cards.Prosperity.TypeClass.CountingHouse && c.CardType != Cards.Prosperity.TypeClass.Forge && c.CardType != Cards.Prosperity.TypeClass.TradeRoute && c.CardType != Cards.Prosperity.TypeClass.Watchtower && c.CardType != Cards.Cornucopia.TypeClass.Remake && c.CardType != Cards.Hinterlands.TypeClass.Develop && c.CardType != Cards.DarkAges.TypeClass.JunkDealer && c.CardType != Cards.DarkAges.TypeClass.Procession && c.CardType != Cards.DarkAges.TypeClass.Rats && c.CardType != Cards.DarkAges.TypeClass.Rebuild)); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = this.FindBestCardToPlay(choice.Cards.Where(c => c.CardType != Cards.Base.TypeClass.Remodel && c.CardType != Cards.Intrigue.TypeClass.Upgrade && c.CardType != Cards.Seaside.TypeClass.Island && c.CardType != Cards.Seaside.TypeClass.Lookout && c.CardType != Cards.Seaside.TypeClass.Salvager && c.CardType != Cards.Seaside.TypeClass.TreasureMap && c.CardType != Cards.Prosperity.TypeClass.TradeRoute && c.CardType != Cards.Cornucopia.TypeClass.Remake && c.CardType != Cards.Hinterlands.TypeClass.Develop && c.CardType != Cards.DarkAges.TypeClass.Rats && c.CardType != Cards.DarkAges.TypeClass.Rebuild)); if (bestCard != null) return new ChoiceResult(new CardCollection() { bestCard }); // Don't play anything return new ChoiceResult(new CardCollection()); } protected override ChoiceResult Decide_Library(Choice choice) { /// TODO -- This should be updated to check to see how many "terminal" Action cards we have vs. how many Actions we have to play them all // If there are no Actions remaining, always set aside Action cards we *want to* play // Otherwise, always keep Action cards if (this.Actions > 0 || !this.ShouldPlay(choice.CardTriggers[0])) return new ChoiceResult(new List() { choice.Options[0] }); else return new ChoiceResult(new List() { choice.Options[1] }); } protected override ChoiceResult Decide_Loan(Choice choice) { // Choose to trash Copper roughly 1/3 of the time (a little odd, but it should work decently) // Let's change this to be a bit more aggressive // If it's a Copper, or if it's a Silver/Talisman/Quarry and we have at least 1 Platinum and at least 3 Ventures, or if it's a Loan and we have fewer than 3 Coppers if (choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper || ((choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Silver || choice.CardTriggers[0].CardType == Cards.Prosperity.TypeClass.Talisman || choice.CardTriggers[0].CardType == Cards.Prosperity.TypeClass.Quarry) && this.CountAll(this, c => c.CardType == Cards.Prosperity.TypeClass.Platinum, true, false) > 0 && this.CountAll(this, c => c.CardType == Cards.Prosperity.TypeClass.Venture, true, false) > 3) || (choice.CardTriggers[0].CardType == Cards.Prosperity.TypeClass.Loan && this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false) < 3)) return new ChoiceResult(new List() { choice.Options[1] }); // Trash else return new ChoiceResult(new List() { choice.Options[0] }); // Discard } protected override ChoiceResult Decide_Lookout(Choice choice) { if (choice.Text == "Choose a card to trash") return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); else return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult Decide_Mandarin(Choice choice) { // Not always the best decision, but for now, it's the easiest if (choice.Text.StartsWith("Choose a card to put back on your deck")) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 1))); } else { CardCollection cards = new CardCollection(choice.Cards); // Order them in roughly random order Utilities.Shuffler.Shuffle(cards); return new ChoiceResult(cards); } } protected override ChoiceResult Decide_Margrave(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult Decide_Masquerade(Choice choice) { if (choice.Text == "Choose a card to pass to the left") { Card masqBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Curse) == Category.Curse); if (masqBestCard == null) masqBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Ruins) == Category.Ruins); if (masqBestCard == null) masqBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Copper); if (masqBestCard == null) masqBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Base.TypeClass.Chapel || c.CardType == Cards.Base.TypeClass.Moneylender || c.CardType == Cards.Base.TypeClass.Remodel || c.CardType == Cards.Intrigue.TypeClass.Masquerade || c.CardType == Cards.Intrigue.TypeClass.TradingPost || c.CardType == Cards.Intrigue.TypeClass.Upgrade || c.CardType == Cards.Seaside.TypeClass.Lookout || c.CardType == Cards.Seaside.TypeClass.Salvager || c.CardType == Cards.Alchemy.TypeClass.Transmute || c.CardType == Cards.Prosperity.TypeClass.Expand || c.CardType == Cards.Prosperity.TypeClass.Forge || c.CardType == Cards.Prosperity.TypeClass.TradeRoute || c.CardType == Cards.Cornucopia.TypeClass.Remake || c.CardType == Cards.Hinterlands.TypeClass.Develop || c.CardType == Cards.DarkAges.TypeClass.Hovel || c.CardType == Cards.DarkAges.TypeClass.OvergrownEstate || c.CardType == Cards.DarkAges.TypeClass.Rats || c.CardType == Cards.DarkAges.TypeClass.Rebuild ); // Last chance -- just take the cheapest one if (masqBestCard == null) masqBestCard = choice.Cards.OrderBy(c => c.BaseCost.Coin.Value + 2.5 * c.BaseCost.Potion.Value).FirstOrDefault(); if (masqBestCard != null) return new ChoiceResult(new CardCollection() { masqBestCard }); return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 1))); } else { Card masqCurse = choice.Cards.FirstOrDefault(c => (c.Category & Category.Curse) == Category.Curse); if (masqCurse != null) return new ChoiceResult(new CardCollection() { masqCurse }); return new ChoiceResult(new CardCollection()); } } protected override ChoiceResult Decide_Mercenary(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: String choiceMercenary = choice.Options[1]; IEnumerable trashableCards = this.FindBestCardsToTrash(this.Hand, 2, true); if (trashableCards.Count() >= 2) choiceMercenary = choice.Options[0]; return new ChoiceResult(new List() { choiceMercenary }); case ChoiceType.Cards: if (choice.Text == "Choose 2 cards to trash") return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(this.Hand, 2, false))); else if (choice.Text.StartsWith("Choose cards to discard.")) return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); else return base.Decide_Mercenary(choice); default: return base.Decide_Mercenary(choice); } } protected override ChoiceResult Decide_Militia(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult Decide_Mine(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: Card mineCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Silver); if (mineCard == null) mineCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Copper); if (mineCard == null) // Pick a random Treasure at this point mineCard = choice.Cards.ElementAt(random.Next(choice.Cards.Count())); return new ChoiceResult(new CardCollection() { mineCard }); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Mine(choice); } } protected override ChoiceResult Decide_MiningVillage(Choice choice) { // Trash if between 4 & 7 Coins available (?? Odd choice) if (this.Currency.Coin > 3 && this.Currency.Coin < 8) return new ChoiceResult(new List() { choice.Options[0] }); // Yes else return new ChoiceResult(new List() { choice.Options[1] }); // No } protected override ChoiceResult Decide_Minion(Choice choice) { // Gain coins if we have another Minion in hand // Gain coins if between 4 & 7 Coins available (?? Odd choice) if (this.Hand[Cards.Intrigue.TypeClass.Minion].Count > 0 || (this.Currency.Coin > 3 && this.Currency.Coin < 8)) return new ChoiceResult(new List() { choice.Options[0] }); // +2 Coins else return new ChoiceResult(new List() { choice.Options[1] }); // Discard Hand } protected override ChoiceResult Decide_Mint(Choice choice) { // Always choose the Treasure card that costs the most to duplicate Card bestCard = null; foreach (Card card in choice.Cards) { if (this._Game.Table.Supplies[card].CanGain() && (bestCard == null || bestCard.Benefit.Currency.Coin < card.Benefit.Currency.Coin || bestCard.Benefit.Currency.Coin == 0)) bestCard = card; } if (bestCard != null) return new ChoiceResult(new CardCollection() { bestCard }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult Decide_Mountebank(Choice choice) { // Discard curse if I don't have a Trader in my hand -- 2 Silvers are better than no Curse card in hand if (this.Hand[Cards.Hinterlands.TypeClass.Trader].Count > 0) return new ChoiceResult(new List() { choice.Options[1] }); // Otherwise, just discard the Curse return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Mystic(Choice choice) { Dictionary _CardsCount = new Dictionary(); foreach (Type cardType in _CardsGained) _CardsCount[cardType] = (int)Math.Pow(CountAll(this, c => c.CardType == cardType, false, true), 2); // Choose one at random, with a probability based on the cards left to be able to draw int indexChosen = random.Next(_CardsCount.Sum(kvp => kvp.Value)); Card mysticCard = null; foreach (Type cardType in _CardsCount.Keys) { if (_CardsCount[cardType] == 0) continue; if (indexChosen < _CardsCount[cardType]) { Supply mysticSupply = choice.Supplies.FirstOrDefault(kvp => kvp.Value.CardType == cardType).Value; if (mysticSupply != null) return new ChoiceResult(mysticSupply); mysticCard = choice.Cards.FirstOrDefault(c => c.CardType == cardType); break; } indexChosen -= _CardsCount[cardType]; } if (mysticCard != null) return new ChoiceResult(new CardCollection() { mysticCard }); return new ChoiceResult(choice.Supplies.ElementAt(random.Next(choice.Supplies.Count)).Value); } protected override ChoiceResult Decide_NativeVillage(Choice choice) { // Retrieve cards from the Native Village mat if there are at least 2 cards there (odd, again...) // Let's change that to if there are more than a uniformly-random number between 2 and 4 // Unless, of course, there are multiple Native Village cards in hand -- then ALWAYS put cards on the Mat if (this.PlayerMats[Cards.Seaside.TypeClass.NativeVillageMat].Count > random.Next(2, 5) && this.Hand[Cards.Seaside.TypeClass.NativeVillage].Count == 0) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Navigator(Choice choice) { /// TO DO -- this logic here! return base.Decide_Navigator(choice); } protected override ChoiceResult Decide_NobleBrigand(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards, 1))); } protected override ChoiceResult Decide_Nobles(Choice choice) { // Choose +2 Actions only if there are fewer Actions than Action cards we want to play if (this.Hand.Count(c => (c.Category & Category.Action) == Category.Action && this.ShouldPlay(c)) > this.Actions) return new ChoiceResult(new List() { choice.Options[1] }); // +2 Actions else return new ChoiceResult(new List() { choice.Options[0] }); // +3 Cards } protected override ChoiceResult Decide_Oasis(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult Decide_Oracle(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: if (choice.PlayerSource == this) { if (this.IsCardOKForMeToDiscard(choice.CardTriggers[0])) return new ChoiceResult(new List() { choice.Options[0] }); else return new ChoiceResult(new List() { choice.Options[1] }); } else { if (!this.IsCardOKForMeToDiscard(choice.CardTriggers[0])) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); } case ChoiceType.Cards: CardCollection oracleCards = new CardCollection(choice.Cards); // Order them in roughly random order Utilities.Shuffler.Shuffle(oracleCards); return new ChoiceResult(oracleCards); default: return base.Decide_Oracle(choice); } } protected override ChoiceResult Decide_Pawn(Choice choice) { // Always choose +1 Coin. Only choose +1 Action if there's at least 1 Action card in hand that we want to play and can play and need the extra Action for List pawnChoices = new List() { choice.Options[3] }; // +1 Coin if (this.Hand.Count(c => (c.Category & Category.Action) == Category.Action && this.ShouldPlay(c)) > this.Actions) pawnChoices.Add(choice.Options[1]); // +1 Action else pawnChoices.Add(choice.Options[0]); // +1 Card return new ChoiceResult(pawnChoices); } protected override ChoiceResult Decide_PearlDiver(Choice choice) { // only put on top if the card has no victory points associated with it (??? What about Harem, Island, etc.?) //if (choice.CardTrigger.VictoryPoints == 0) // Only put on top if the card is not a Victory (only Victory -- not dual-purpose) or Curse or Copper card if (choice.CardTriggers[0].Category == Category.Victory || choice.CardTriggers[0].Category == Category.Curse || (choice.CardTriggers[0].Category & Category.Ruins) == Category.Ruins || choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper || choice.CardTriggers[0].CardType == Cards.DarkAges.TypeClass.OvergrownEstate || choice.CardTriggers[0].CardType == Cards.DarkAges.TypeClass.Hovel) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Pillage(Choice choice) { // First priority is Platinum if (choice.Cards.Count(c => c.CardType == Cards.Prosperity.TypeClass.Platinum) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => c.CardType == Cards.Prosperity.TypeClass.Platinum) }); // Next priority is King's Court if the player has Action cards other than KC/TR else if (choice.Cards.Count(c => c.CardType == Cards.Prosperity.TypeClass.KingsCourt) > 0 && choice.Cards.Count(c => (c.Category & Category.Action) == Category.Action && c.CardType != Cards.Prosperity.TypeClass.KingsCourt && c.CardType != Cards.Base.TypeClass.ThroneRoom) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => c.CardType == Cards.Prosperity.TypeClass.KingsCourt) }); // Next priority is 5-cost+ Attack cards else if (choice.Cards.Count(c => (c.Category & Category.Attack) == Category.Attack && c.BaseCost.Coin >= 5) > 0) return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards.Where(c => (c.Category & Category.Attack) == Category.Attack && c.BaseCost.Coin >= 5), 1))); // Next priority is Gold else if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Gold) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => c.CardType == Cards.Universal.TypeClass.Gold) }); // Next priority is 5-cost+ Action/Treasure cards (other than Ill-Gotten Gains) else if (choice.Cards.Count(c => ((c.Category & Category.Action) == Category.Action || (c.Category & Category.Treasure) == Category.Treasure) && c.BaseCost.Coin >= 5 && c.CardType != Cards.Hinterlands.TypeClass.IllGottenGains) > 0) return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards.Where(c => ((c.Category & Category.Action) == Category.Action || (c.Category & Category.Treasure) == Category.Treasure) && c.BaseCost.Coin >= 5 && c.CardType != Cards.Hinterlands.TypeClass.IllGottenGains), 1))); // Next priority is any remaining Attack cards else if (choice.Cards.Count(c => (c.Category & Category.Attack) == Category.Attack) > 0) return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards.Where(c => (c.Category & Category.Attack) == Category.Attack), 1))); // Next priority is any remaining Action/Treasure cards else if (choice.Cards.Count(c => (c.Category & Category.Action) == Category.Action || (c.Category & Category.Treasure) == Category.Treasure) > 0) return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards.Where(c => (c.Category & Category.Action) == Category.Action || (c.Category & Category.Treasure) == Category.Treasure), 1))); // Final fall-through return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards, 1))); } protected override ChoiceResult Decide_PirateShip(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: IEnumerator ePlayers = this._Game.GetPlayersStartingWithActiveEnumerator(); ePlayers.MoveNext(); int blockedCount = 0; while (ePlayers.MoveNext()) { Player attackee = ePlayers.Current; if (attackee.PreviousTableau[Cards.Seaside.TypeClass.Lighthouse].Count > 0 || (this.KnownPlayerHands.ContainsKey(attackee) && this.KnownPlayerHands[attackee].Any(c => c.CardType == Cards.Base.TypeClass.Moat))) blockedCount++; } // Take the Pirate Ship tokens if all attacks have been blocked -- no point in attacking if (blockedCount == this._Game.Players.Count - 1) return new ChoiceResult(new List() { choice.Options[1] }); // Steal coins until I have at least 3 Pirate Ship tokens on my mat else if (this.TokenPiles[Cards.Seaside.TypeClass.PirateShipToken].Count > 3) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards, 1))); default: return base.Decide_Remodel(choice); } } protected override ChoiceResult Decide_Rabble(Choice choice) { CardCollection cards = new CardCollection(choice.Cards); // Order them in roughly random order Utilities.Shuffler.Shuffle(cards); return new ChoiceResult(cards); } protected override ChoiceResult Decide_Rats(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_Rebuild(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.SuppliesAndCards: Boolean colonyExists = false; Boolean colonyAvailable = false; int colonyCount = 0; if (this._Game.Table.Supplies.ContainsKey(Cards.Prosperity.TypeClass.Colony)) { colonyExists = true; colonyAvailable = this._Game.Table.Supplies[Cards.Prosperity.TypeClass.Colony].CanGain(); colonyCount = this.CountAll(this, c => c.CardType == Cards.Prosperity.TypeClass.Colony, true, true); } Boolean provinceAvailable = this._Game.Table.Province.CanGain(); int provinceCount = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Province, true, true); Boolean duchyAvailable = this._Game.Table.Province.CanGain(); int duchyCount = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Duchy, true, true); Boolean estateAvailable = this._Game.Table.Province.CanGain(); int estateCount = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Estate, true, true); Supply victorySupply = null; if (colonyExists && (colonyCount > 0 || colonyAvailable)) victorySupply = choice.Supplies.First(kvp => kvp.Value.CardType == Cards.Prosperity.TypeClass.Colony).Value; if (victorySupply == null && provinceCount > 0) victorySupply = choice.Supplies.First(kvp => kvp.Value.CardType == Cards.Universal.TypeClass.Province).Value; if (victorySupply == null) { victorySupply = choice.Supplies.Select(kvp => kvp.Value).Where(s => (s.Category & Category.Victory) == Category.Victory && s.CardType != Cards.Hinterlands.TypeClass.Farmland) .OrderByDescending(s => s.BaseCost).First(); } return new ChoiceResult(victorySupply); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Rebuild(choice); } } protected override ChoiceResult Decide_Remake(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Remake(choice); } } protected override ChoiceResult Decide_Remodel(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Remodel(choice); } } protected override ChoiceResult Decide_Rogue(Choice choice) { if (choice.Text == "Choose a card to gain from the trash") return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); else if (choice.Text == "Choose a card to trash") return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); else return base.Decide_Rogue(choice); } protected override ChoiceResult Decide_Saboteur(Choice choice) { Supply bestSupply = FindBestCardForCost(choice.Supplies.Values, null, false); if (this.Hand[Cards.Hinterlands.TypeClass.Trader].Count > 0 && this._Game.Table.Silver.CanGain()) { // This is the only instance where we'll "gain" whatever we can (even a Curse) } else { // Never, ever gain a Curse if (bestSupply.CardType == Cards.Universal.TypeClass.Curse) bestSupply = null; // Never, ever gain a Ruins else if (bestSupply.CardType == Cards.DarkAges.TypeClass.RuinsSupply) bestSupply = null; else if (bestSupply.CardType == Cards.Universal.TypeClass.Copper) { // Only ever gain a Copper in specific situations (Counting House, Coppersmith, Gardens, etc.) int copperUsingCards = this.CountAll(this, c => c.CardType == Cards.Base.TypeClass.Gardens || c.CardType == Cards.Base.TypeClass.Moneylender || c.CardType == Cards.Intrigue.TypeClass.Coppersmith || c.CardType == Cards.Alchemy.TypeClass.Apothecary || c.CardType == Cards.Prosperity.TypeClass.CountingHouse, true, false); int copperCards = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false); int treasureCards = this.CountAll(this, c => (c.Category & Category.Treasure) == Category.Treasure, true, false); int totalCards = this.CountAll(); if (((float)copperUsingCards / totalCards < 0.20 && (float)copperCards / totalCards > 0.40) || (float)copperCards / treasureCards < 0.30) bestSupply = null; } } return new ChoiceResult(bestSupply); } protected override ChoiceResult Decide_Salvager(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_Scavenger(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // I have no freaking clue... -- just choose at random return new ChoiceResult(new List() { choice.Options[random.Next(0, 1)] }); case ChoiceType.Cards: // Take the best card (? I'unno... seems OK-ish) return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards, 1))); default: return base.Decide_Scavenger(choice); } } protected override ChoiceResult Decide_Scheme(Choice choice) { // Always take the most expensive card return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards, 1))); } protected override ChoiceResult Decide_Scout(Choice choice) { CardCollection scoutCards = new CardCollection(choice.Cards); // Order them in roughly random order Utilities.Shuffler.Shuffle(scoutCards); return new ChoiceResult(scoutCards); } protected override ChoiceResult Decide_ScryingPool(Choice choice) { if (choice.PlayerSource == this) { if (IsCardOKForMeToDiscard(choice.CardTriggers[0]) && (choice.CardTriggers[0].Category & Category.Ruins) != Category.Ruins) return new ChoiceResult(new List() { choice.Options[0] }); else return new ChoiceResult(new List() { choice.Options[1] }); } else { if (!IsCardOKForMeToDiscard(choice.CardTriggers[0])) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); } } protected override ChoiceResult Decide_SecretChamber(Choice choice) { if (choice.Text == "Choose order of cards to put back on your deck") { // Order all the cards IEnumerable cardsToReturn = this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count()); // Try to save 1 Curse if we can if (choice.CardTriggers[0].CardType == Cards.Prosperity.TypeClass.Mountebank) { cardsToReturn = cardsToReturn.Take(3); if (cardsToReturn.ElementAt(0).CardType == Cards.Universal.TypeClass.Curse) return new ChoiceResult(new CardCollection(cardsToReturn.Skip(1))); if (cardsToReturn.ElementAt(1).CardType == Cards.Universal.TypeClass.Curse) return new ChoiceResult(new CardCollection() { cardsToReturn.ElementAt(0), cardsToReturn.ElementAt(2)}); } // Try to not put Treasure cards onto our deck, even if that means putting Action cards there else if (choice.CardTriggers[0].CardType == Cards.Seaside.TypeClass.PirateShip) { CardCollection pirateShipCards = new CardCollection(cardsToReturn.Where(c => (c.Category & Category.Treasure) != Category.Treasure)); if (pirateShipCards.Count < 2) pirateShipCards.AddRange(cardsToReturn.Where(c => (c.Category & Category.Treasure) != Category.Treasure).Take(2 - pirateShipCards.Count)); return new ChoiceResult(pirateShipCards); } return new ChoiceResult(new CardCollection(cardsToReturn.Take(2))); } else { CardCollection scCards = new CardCollection(); foreach (Card card in choice.Cards) { if (card.Category == Category.Curse || ((card.Category & Category.Victory) == Category.Victory && (card.Category & Category.Treasure) != Category.Treasure) || (card.CardType == Cards.Universal.TypeClass.Copper && this.Tableau[Cards.Intrigue.TypeClass.Coppersmith].Count == 0) || (this.Actions == 0 && (card.Category & Category.Treasure) != Category.Treasure)) scCards.Add(card); } return new ChoiceResult(scCards); } } protected override ChoiceResult Decide_SirBailey(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_SirDestry(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_SirMartin(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_SirMichael(Choice choice) { if (choice.Text.StartsWith("Choose cards to discard.")) return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); else return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_SirVander(Choice choice) { return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_Smugglers(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_SpiceMerchant(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // To fall in line with Pawn, always take the Coins & Buy return new ChoiceResult(new List() { choice.Options[1] }); case ChoiceType.Cards: // Only ever trash Coppers Card smCopper = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Copper); if (smCopper != null) return new ChoiceResult(new CardCollection() { smCopper }); return new ChoiceResult(new CardCollection()); default: return base.Decide_SpiceMerchant(choice); } } protected override ChoiceResult Decide_Spy(Choice choice) { if (choice.PlayerSource == this) { if (this.IsCardOKForMeToDiscard(choice.CardTriggers[0])) return new ChoiceResult(new List() { choice.Options[0] }); // Discard else return new ChoiceResult(new List() { choice.Options[1] }); // Put back } else { if (!this.IsCardOKForMeToDiscard(choice.CardTriggers[0])) return new ChoiceResult(new List() { choice.Options[1] }); // Put back else return new ChoiceResult(new List() { choice.Options[0] }); // Discard } } protected override ChoiceResult Decide_Squire(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // Count the number of Action cards in my Hand, Deck, & Discard pile. // If there is at least 2 in my hand then choose +2 Actions. Otherwise, choose Gain a Silver List squireChoices = new List() { choice.Options[2] }; if (this.Hand[Category.Action].Count >= 2) squireChoices[0] = choice.Options[0]; return new ChoiceResult(squireChoices); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Squire(choice); } } protected override ChoiceResult Decide_Stables(Choice choice) { // Always discard Copper Card stablesBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Copper); if (stablesBestCard == null && (this.Hand[Cards.Alchemy.TypeClass.Potion].Count > 1 || _Game.Table.Supplies.Count(kvp => kvp.Value.BaseCost.Potion > 0 && kvp.Value.CanGain()) < 1)) stablesBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Alchemy.TypeClass.Potion); if (stablesBestCard == null && this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false) < 4) stablesBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Prosperity.TypeClass.Loan); if (stablesBestCard == null && this.Hand[Cards.Hinterlands.TypeClass.FoolsGold].Count == 1) stablesBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Hinterlands.TypeClass.FoolsGold); if (stablesBestCard != null) return new ChoiceResult(new CardCollection() { stablesBestCard }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult Decide_Stash(Choice choice) { // For now, always put Stash on top of the draw pile // This is very bad for instances involving Thief or Saboteur, but for now, it'll function CardCollection cards = new CardCollection(); cards.AddRange(choice.Cards.Where(c => c.CardType == Cards.Promotional.TypeClass.Stash)); cards.AddRange(choice.Cards.Where(c => c.CardType != Cards.Promotional.TypeClass.Stash)); return new ChoiceResult(cards); } protected override ChoiceResult Decide_Steward(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // Trash 2 cards if we have 2 Curse/Ruins cards if (this.Hand[Category.Curse].Count + this.Hand[Category.Ruins].Count >= 2) return new ChoiceResult(new List() { choice.Options[2] }); // Otherwise, take 2 Coins if we have at least 3 already else if (this.Currency.Coin >= 3) return new ChoiceResult(new List() { choice.Options[1] }); // Otherwise, just draw 2 cards else return new ChoiceResult(new List() { choice.Options[0] }); case ChoiceType.Cards: // Trashing cards if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Curse || (c.Category & Category.Ruins) == Category.Ruins) >= 2) return new ChoiceResult(new CardCollection(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Curse || (c.Category & Category.Ruins) == Category.Ruins ).Take(2))); else return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 2))); default: return base.Decide_Steward(choice); } } protected override ChoiceResult Decide_Storeroom(Choice choice) { /// TODO -- this needs to be a bit smarter. It shouldn't discard Action cards if we have 1+ Actions left /// It can also be improved to chain with Tactician (and so can Secret Chamber, for that matter) // Cards for cards (Cellar) if (choice.Text.Contains("+1 Card")) { if (this.Actions == 0) // Discard all non-Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => (c.Category & Category.Treasure) != Category.Treasure))); else // Discard all non-Action/Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => (c.Category & Category.Action) != Category.Action && (c.Category & Category.Treasure) != Category.Treasure))); } // Cards for coins (Secret Chamber/Vault) else // "+1" { if (this.Actions == 0) // Discard all non-Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => (c.Category & Category.Treasure) != Category.Treasure))); else // Discard all non-Action/Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => (c.Category & Category.Action) != Category.Action && (c.Category & Category.Treasure) != Category.Treasure))); } } protected override ChoiceResult Decide_Swindler(Choice choice) { return new ChoiceResult(FindWorstCardForCost(choice.Supplies.Values, null)); } protected override ChoiceResult Decide_Thief(Choice choice) { if (choice.Text.StartsWith("Choose a Treasure card of")) { return new ChoiceResult(new CardCollection(this.FindBestCards(choice.Cards, 1))); } // Always gain all Treasure cards else if (choice.Text.StartsWith("Choose which cards you'd like to gain")) { CardCollection ccThief = new CardCollection(choice.Cards.Where(c => c.CardType != Cards.Universal.TypeClass.Copper)); int coppers = this.CountAll(this, c => c.CardType == Cards.Universal.TypeClass.Copper, true, false); int allTreasures = this.CountAll(this, c => (c.Category & Category.Treasure) == Category.Treasure, true, false); double percentageCoppers = ((double)coppers) / allTreasures; // Don't gain Loan if we don't have many Coppers if (percentageCoppers < 0.1 || (coppers < 3 && percentageCoppers < 0.4)) ccThief.RemoveAll(c => c.CardType == Cards.Prosperity.TypeClass.Loan); return new ChoiceResult(ccThief); } return base.Decide_Thief(choice); } protected override ChoiceResult Decide_ThroneRoom(Choice choice) { Card bestCard = this.FindBestCardToPlay(choice.Cards.Where(c => c.CardType != Cards.Base.TypeClass.Chapel && c.CardType != Cards.Base.TypeClass.Library && c.CardType != Cards.Base.TypeClass.Remodel && c.CardType != Cards.Intrigue.TypeClass.SecretChamber && c.CardType != Cards.Intrigue.TypeClass.Upgrade && c.CardType != Cards.Seaside.TypeClass.Island && c.CardType != Cards.Seaside.TypeClass.Lookout && c.CardType != Cards.Seaside.TypeClass.Outpost && c.CardType != Cards.Seaside.TypeClass.Salvager && c.CardType != Cards.Seaside.TypeClass.Tactician && c.CardType != Cards.Seaside.TypeClass.TreasureMap && c.CardType != Cards.Prosperity.TypeClass.CountingHouse && c.CardType != Cards.Prosperity.TypeClass.Forge && c.CardType != Cards.Prosperity.TypeClass.TradeRoute && c.CardType != Cards.Prosperity.TypeClass.Watchtower && c.CardType != Cards.Cornucopia.TypeClass.Remake && c.CardType != Cards.Hinterlands.TypeClass.Develop && c.CardType != Cards.DarkAges.TypeClass.JunkDealer && c.CardType != Cards.DarkAges.TypeClass.Procession && c.CardType != Cards.DarkAges.TypeClass.Rats && c.CardType != Cards.DarkAges.TypeClass.Rebuild)); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = this.FindBestCardToPlay(choice.Cards.Where(c => c.CardType != Cards.Base.TypeClass.Remodel && c.CardType != Cards.Intrigue.TypeClass.Upgrade && c.CardType != Cards.Seaside.TypeClass.Island && c.CardType != Cards.Seaside.TypeClass.Lookout && c.CardType != Cards.Seaside.TypeClass.Salvager && c.CardType != Cards.Seaside.TypeClass.TreasureMap && c.CardType != Cards.Prosperity.TypeClass.TradeRoute && c.CardType != Cards.Cornucopia.TypeClass.Remake && c.CardType != Cards.Hinterlands.TypeClass.Develop && c.CardType != Cards.DarkAges.TypeClass.Rats && c.CardType != Cards.DarkAges.TypeClass.Rebuild)); if (bestCard != null) return new ChoiceResult(new CardCollection() { bestCard }); return new ChoiceResult(new CardCollection() { choice.Cards.ElementAt(random.Next(choice.Cards.Count())) }); } protected override ChoiceResult Decide_Tournament(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // Always reveal a Province if I can return new ChoiceResult(new List() { choice.Options[0] }); case ChoiceType.Cards: // Prioritize card worth based on my own metric // 1. Duchy if Game Progress is 0.4 or less (closeish to the end) // 2. Trusty Steed // 3. Followers // 4. Princess // 5. Bag of Gold // 6. Diadem // 7. Duchy Card bestCard = null; if (this.GameProgress < 0.4 && this._Game.Table[Cards.Universal.TypeClass.Duchy].Count > 0) bestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Duchy); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Cornucopia.TypeClass.TrustySteed); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Cornucopia.TypeClass.Followers); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Cornucopia.TypeClass.Princess); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Cornucopia.TypeClass.BagOfGold); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Cornucopia.TypeClass.Diadem); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Duchy); if (bestCard != null) return new ChoiceResult(new CardCollection() { bestCard }); return new ChoiceResult(new CardCollection()); default: return base.Decide_Tournament(choice); } } protected override ChoiceResult Decide_Torturer(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: int torturerCrapCards = this.Hand.Count(card => card.CardType == Cards.Universal.TypeClass.Copper || card.Category == Category.Victory || card.Category == Category.Curse || (card.Category & Category.Ruins) == Category.Ruins); // Choose to take a Curse if there aren't any left if (this._Game.Table.Supplies[Cards.Universal.TypeClass.Curse].Count == 0) return new ChoiceResult(new List() { choice.Options[1] }); // Choose to discard 2 cards if we have at least 2 Copper, Victory, Curse, and/or Ruins cards, or if that's all our hand is else if (torturerCrapCards >= 2 || this.Hand.Count == torturerCrapCards) return new ChoiceResult(new List() { choice.Options[0] }); // Choose to take on a Curse else return new ChoiceResult(new List() { choice.Options[1] }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 2))); default: return base.Decide_Torturer(choice); } } protected override ChoiceResult Decide_Trader(Choice choice) { // Always trash Curses if we can Card traderBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Curse) == Category.Curse); // Always trash Ruins if we can if (traderBestCard == null) traderBestCard = choice.Cards.FirstOrDefault(c => (c.Category & Category.Ruins) == Category.Ruins); // Trash Copper later in the game -- they just suck if (traderBestCard == null && this.GameProgress < 0.75) traderBestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Copper); if (traderBestCard == null) traderBestCard = this.FindBestCardsToTrash(choice.Cards.Where(c => (c.Category & Category.Victory) != Category.Victory), 1).FirstOrDefault(); if (traderBestCard == null) traderBestCard = this.FindBestCardsToTrash(choice.Cards, 1).ElementAt(0); if (traderBestCard != null) return new ChoiceResult(new CardCollection() { traderBestCard }); return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_TradeRoute(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_TradingPost(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 2))); } protected override ChoiceResult Decide_Transmute(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult Decide_TrustySteed(Choice choice) { // Count the number of Action cards in my Hand, Deck, & Discard pile. // If there is at least 2 in my hand or there is a decent probability that I'll get 2 with drawing, // then choose +2 Cards & +2 Actions. Otherwise, choose +2 Cards & +2 Coin. List trustySteedChoices = new List() { choice.Options[0] }; if (this.Hand[Category.Action].Count >= 2) trustySteedChoices.Add(choice.Options[1]); else { int actionCardsAvailable = this.CountAll(this, c => (c.Category & Category.Action) == Category.Action, true, true); int totalCards = this.DrawPile.Count + this.DiscardPile.Count; double chanceOfGettingActions; if (this.Hand[Category.Action].Count == 1) chanceOfGettingActions = 1.0 - ((double)(totalCards - actionCardsAvailable) / totalCards) * ((double)(totalCards - actionCardsAvailable) / (totalCards - 1)); else chanceOfGettingActions = ((double)(actionCardsAvailable) / totalCards) * ((double)(actionCardsAvailable - 1) / (totalCards - 1)); // We'll be slightly optimistic here -- this may need some tweaking) if (chanceOfGettingActions > 0.40) trustySteedChoices.Add(choice.Options[1]); else trustySteedChoices.Add(choice.Options[2]); } return new ChoiceResult(trustySteedChoices); } protected override ChoiceResult Decide_University(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Upgrade(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); default: return base.Decide_Upgrade(choice); } } protected override ChoiceResult Decide_Vault(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // If there are at least 2 non-Action & non-Treasure cards, discard 2 of them IEnumerable vaultDiscardableCards = this.Hand[c => (c.Category & Category.Treasure) != Category.Treasure && (c.Category & Category.Action) != Category.Action]; if (vaultDiscardableCards.Count() >= 2) return new ChoiceResult(new List() { choice.Options[0] }); else return new ChoiceResult(new List() { choice.Options[1] }); case ChoiceType.Cards: if (choice.Text.StartsWith("Discard any number of cards")) { /// TODO -- this needs to be a bit smarter. It shouldn't discard Action cards if we have 1+ Actions left /// It can also be improved to chain with Tactician (and so can Secret Chamber, for that matter) // Discard all non-Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => (c.Category & Category.Treasure) != Category.Treasure))); } else // "Choose 2 cards to discard" { CardCollection vDiscards = new CardCollection(); vDiscards.AddRange(choice.Cards.Where(c => (c.Category & Category.Curse) == Category.Curse)); if (vDiscards.Count < 2) vDiscards.AddRange(choice.Cards.Where(card => (card.Category & Category.Ruins) == Category.Ruins)); if (vDiscards.Count < 2) vDiscards.AddRange(choice.Cards.Where(card => card.Category == Category.Victory)); if (vDiscards.Count > 2) vDiscards.RemoveRange(2, vDiscards.Count - 2); else vDiscards.AddRange(this.FindBestCardsToDiscard(choice.Cards, 2 - vDiscards.Count)); return new ChoiceResult(vDiscards); } default: return base.Decide_Vault(choice); } } protected override ChoiceResult Decide_WanderingMinstrel(Choice choice) { // Just sort everything best-to-worst CardCollection cards = new CardCollection(choice.Cards); cards.Sort(new DominionBase.Cards.Sorting.ByCost(DominionBase.Cards.Sorting.SortDirection.Descending)); return new ChoiceResult(cards); } protected override ChoiceResult Decide_Warehouse(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 3))); } protected override ChoiceResult Decide_Watchtower(Choice choice) { // Always trash Curse & Copper cards from a Watchtower if (choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Curse || choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List() { choice.Options[0] }); // Almost always trash Ruins (only if we have Death Cart do we want to keep them) else if ((choice.CardTriggers[0].Category & Category.Ruins) == Category.Ruins && this.CountAll(this, c => c.CardType == Cards.DarkAges.TypeClass.DeathCart, true, false) == 0) return new ChoiceResult(new List() { choice.Options[0] }); else return new ChoiceResult(new List() { choice.Options[1] }); } protected override ChoiceResult Decide_WishingWell(Choice choice) { Dictionary _CardsCount = new Dictionary(); foreach (Type cardType in _CardsGained) _CardsCount[cardType] = (int)Math.Pow(CountAll(this, c => c.CardType == cardType, false, true), 2); // Choose one at random, with a probability based on the cards left to be able to draw int indexChosen = random.Next(_CardsCount.Sum(kvp => kvp.Value)); Card wishingWellCard = null; foreach (Type cardType in _CardsCount.Keys) { if (_CardsCount[cardType] == 0) continue; if (indexChosen < _CardsCount[cardType]) { Supply wishingWellSupply = choice.Supplies.FirstOrDefault(kvp => kvp.Value.CardType == cardType).Value; if (wishingWellSupply != null) return new ChoiceResult(wishingWellSupply); wishingWellCard = choice.Cards.FirstOrDefault(c => c.CardType == cardType); break; } indexChosen -= _CardsCount[cardType]; } if (wishingWellCard != null) return new ChoiceResult(new CardCollection() { wishingWellCard }); return base.Decide_WishingWell(choice); } protected override ChoiceResult Decide_Workshop(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_YoungWitch(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 2))); } } }