using System; using System.Collections.Generic; using System.Linq; using System.Text; using DominionBase.Cards; using DominionBase.Piles; namespace DominionBase.Players.AI { /// /// The jatill AI is as close to the same as the AI that jatill did for his application /// public class jatill : Basic { public new static String AIName { get { return "jatill's (old)"; } } public new static String AIDescription { get { return "Makes decisions based on jatill's code from back in February 2011 (now very dated). Will perform worse & worse as expansions are released."; } } public jatill(Game game, String name) : base(game, name) { } protected override Card FindBestCardToPlay(IEnumerable cards) { if (this.Phase == PhaseEnum.Action) { // 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) return kc; // Always play Throne Room if there is one (?) Card tr = cards.FirstOrDefault(card => card.CardType == Cards.Base.TypeClass.ThroneRoom); if (tr != null) return tr; Card plusActions = cards.FirstOrDefault(card => card.Benefit.Actions > 0 && card.CardType != Cards.Intrigue.TypeClass.ShantyTown); if (plusActions != null) return plusActions; Card shantyTown = cards.FirstOrDefault(card => card.CardType == Cards.Intrigue.TypeClass.ShantyTown); if (shantyTown != null) return shantyTown; 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) 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())) return cards.First(card => card.CardType == Cards.Seaside.TypeClass.Smugglers); // Play an Ambassador card if there is one and we have at least 1 Curse in our hand Card ambassador = cards.FirstOrDefault(card => card.CardType == Cards.Seaside.TypeClass.Ambassador); if (ambassador != null) return ambassador; if (cards.Count() > 0) // Just play the most expensive one return cards.ElementAt(0); return null; } return base.FindBestCardToPlay(cards); } protected override CardCollection FindBestCardsToPlay(IEnumerable cards) { if (this.Phase == PhaseEnum.Treasure) { IEnumerable t = cards.Where(c => c.CardType != Cards.Prosperity.TypeClass.Contraband || c.CardType != Cards.Prosperity.TypeClass.Bank); if (t.Count() > 0) return new CardCollection(t); t = cards.Where(c => c.CardType == Cards.Prosperity.TypeClass.Bank); if (t.Count() > 0) return new CardCollection(t); return new CardCollection(); } return base.FindBestCardsToPlay(cards); } protected override bool ShouldBuy(Type type) { if (type == Cards.Universal.TypeClass.Curse) return false; else if (type == Cards.Base.TypeClass.Adventurer) return false; else if (type == Cards.Base.TypeClass.Moneylender) return false; else if (type == Cards.Base.TypeClass.Woodcutter) return false; else if (type == Cards.Base.TypeClass.Chancellor) return false; else if (type == Cards.Base.TypeClass.Chapel) return false; else if (type == Cards.Base.TypeClass.Spy) 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.Bridge) 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.Conspirator) 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.Prosperity.TypeClass.Contraband) 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.GrandMarket) return false; else if (type == Cards.Prosperity.TypeClass.Loan) return false; else if (type == Cards.Prosperity.TypeClass.TradeRoute) return false; else if (type == Cards.Prosperity.TypeClass.WorkersVillage) return false; else if (type == Cards.Cornucopia.TypeClass.Remake) return false; return true; } protected override Supply FindBestCardToBuy(List buyableSupplies) { return this.FindBestCardForCost(buyableSupplies, this.Currency, true); } protected override Supply FindBestCardForCost(IEnumerable buyableSupplies, Currency currency, Boolean buying) { List bestSupplies = new List(); Cost bestCost = null; // Buy a potion if we don't have any and we can (??) if (this.Currency != (Currency)null && (this.Currency.Coin == 4 && this.CountAll(this, c => c.CardType == Cards.Alchemy.TypeClass.Potion, true, false) == 0)) { Supply potion = buyableSupplies.SingleOrDefault(supply => supply.CardType == Cards.Alchemy.TypeClass.Potion); if (potion != null) return potion; } foreach (Supply supply in buyableSupplies) { if (!ShouldBuy(supply)) continue; // Only return ones we CAN gain if (currency != (Currency)null && currency < supply.CurrentCost) continue; // If the card chosen is under embargo, then possibly skip it if (buying && random.Next(supply.Tokens.Count(t => t.GetType() == Cards.Seaside.TypeClass.EmbargoToken)) != 0) continue; // If we chose Peddler, but it costs more than 5, then possibly skip it if (supply.CardType == Cards.Prosperity.TypeClass.Peddler && supply.CurrentCost.Coin > 5 && random.Next(10) != 0) 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.4 && supply.SupplyCardType == Cards.Universal.TypeClass.Duchy)) 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 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 override IEnumerable FindBestCardsToDiscard(IEnumerable cards, int count) { // choose the worse card in hand in this order // 1) positive victory points // 2) curse // 3) cheapest card left CardCollection cardsToDiscard = new CardCollection(); CardCollection cardsLeftOver = new CardCollection(); foreach (Card card in cards) { if (card.Category == Category.Victory) cardsToDiscard.Add(card); else cardsLeftOver.Add(card); if (cardsToDiscard.Count >= count) break; } if (cardsToDiscard.Count >= count) return cardsToDiscard; cardsToDiscard.AddRange(FindBestCardsToTrash(cardsLeftOver, count - cardsToDiscard.Count)); return cardsToDiscard; } protected override IEnumerable FindBestCardsToTrash(IEnumerable cards, int count) { // choose the worse card in hand in this order // 1) curse // 2) 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.OrderBy(c => c.BaseCost).ThenBy(c => c.Name).Where(c => !cardsToTrash.Contains(c)).Take(count - cardsToTrash.Count)); return cardsToTrash; } 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.Mine) { // Mine can only be played if you have a copper or silver in hand, // and the next highest coin is available if ((this.Hand[Cards.Universal.TypeClass.Copper].Count > 0 && this._Game.Table.Supplies[Cards.Universal.TypeClass.Silver].Count > 0) || (this.Hand[Cards.Universal.TypeClass.Silver].Count > 0 && this._Game.Table.Supplies[Cards.Universal.TypeClass.Gold].Count > 0)) 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 Estate, Duchy, Province, or Curse in hand if (this.Hand.Count( c => c.CardType == Cards.Universal.TypeClass.Estate || c.CardType == Cards.Universal.TypeClass.Duchy || c.CardType == Cards.Universal.TypeClass.Province || c.CardType == Cards.Universal.TypeClass.Curse) <= 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.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) { if (this.Hand[Category.Treasure].Count == 0) return false; } return true; } protected override ChoiceResult Decide_Attacked(Choice choice, AttackedEventArgs aea, IEnumerable cardsToReveal) { if (cardsToReveal.Contains(Cards.Base.TypeClass.Moat) && !aea.Cancelled) return new ChoiceResult(new CardCollection() { aea.Revealable[Cards.Base.TypeClass.Moat].Card }); // Always reveal Horse Traders (any reason *not* to?) if (cardsToReveal.Contains(Cards.Cornucopia.TypeClass.HorseTraders)) return new ChoiceResult(new CardCollection() { aea.Revealable[Cards.Cornucopia.TypeClass.HorseTraders].Card }); if (cardsToReveal.Contains(Cards.Intrigue.TypeClass.SecretChamber) && !aea.HandledBy.Contains(Cards.Intrigue.TypeClass.SecretChamber) && (_LastReactedCard == null || _LastReactedCard != choice.CardTriggers[0])) return new ChoiceResult(new CardCollection() { aea.Revealable[Cards.Intrigue.TypeClass.SecretChamber].Card }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult Decide_CardGain(Choice choice, CardGainEventArgs cgea, IEnumerable cardTriggerTypes) { // Always put card on top of your deck if (cardTriggerTypes.Contains(Cards.Prosperity.TypeClass.RoyalSeal)) 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].CardType == Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List() { cgea.Actions[Cards.Prosperity.TypeClass.Watchtower].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)) { if (this.Tableau[Category.Treasure].Any(c => c.CardType != Cards.Universal.TypeClass.Copper)) 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 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_RevealBane(Choice choice) { // Always reveal the Bane card if I can return new ChoiceResult(new List() { choice.Options[0] }); } 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_Apprentice(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 1))); } 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.")) { // 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) }); else return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(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) }); 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_Cellar(Choice choice) { // Discard all non-Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => (c.Category & Category.Treasure) != Category.Treasure))); } protected override ChoiceResult Decide_Chancellor(Choice choice) { // Always discard deck -- ??? That seems a bit odd... return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Chapel(Choice choice) { // Always choose to trash all Curses if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Curse) > 0) return new ChoiceResult(new CardCollection(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Curse).Take(4))); else 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 int remainingCoins = Math.Max(0, 2 * choice.PlayerSource.Hand.Count + (int)(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_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_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_Envoy(Choice choice) { // Find most-expensive non-Victory card to discard IEnumerable cardsEnvoy = this.FindBestCards(choice.Cards.Where(c => c.Category != Category.Curse && c.Category != Category.Victory), 1); if (cardsEnvoy != null && cardsEnvoy.Count() > 0) return new ChoiceResult(new CardCollection(cardsEnvoy)); 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: 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_Farmland(choice); } } protected override ChoiceResult Decide_Feast(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Forge(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Cards: // Only trash Curses 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) }); else return new ChoiceResult(new CardCollection()); 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_Goons(Choice choice) { return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult Decide_Haggler(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } 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 (havenTreasures.Count() > 0) 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_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 1/2 the time after that if (this.GameProgress > 0.35 || random.Next(2) == 0) 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_Ironworks(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Island(Choice choice) { if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Estate) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => c.CardType == Cards.Universal.TypeClass.Estate) }); else if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Duchy) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => c.CardType == Cards.Universal.TypeClass.Duchy) }); else if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Province) > 0) return new ChoiceResult(new CardCollection() { choice.Cards.First(c => c.CardType == Cards.Universal.TypeClass.Province) }); else 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) }); return base.Decide_Island(choice); } protected override ChoiceResult Decide_KingsCourt(Choice choice) { Card bestCard = this.FindBestCardToPlay(choice.Cards); if (bestCard != null) return new ChoiceResult(new CardCollection() { this.FindBestCardToPlay(choice.Cards) }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult Decide_Library(Choice choice) { // Always set aside Action cards return new ChoiceResult(new List() { choice.Options[0] }); } 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) if (choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper && random.Next(3) == 0) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); } 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 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_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) { // Always discard curse if I can (better to not if I can trash it, but I never buy trashing cards anyway) return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_NativeVillage(Choice choice) { // Retrieve cards from the Native Village mat if there are at least 2 cards there if (this.PlayerMats[Cards.Seaside.TypeClass.NativeVillageMat].Count >= 2) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_Nobles(Choice choice) { // Choose +2 Actions only if there are at least 2 Action cards in hand already if (this.Hand[Category.Action].Count >= 2) 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_Pawn(Choice choice) { // Always choose +1 Coin. Only choose +1 Action if there's at least 1 Action card in hand List pawnChoices = new List() { choice.Options[3] }; // +1 Coin if (this.Hand[Category.Action].Count >= 1) 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 if ((choice.CardTriggers[0].Category & Category.Victory) == Category.Victory || choice.CardTriggers[0].Category == Category.Curse) return new ChoiceResult(new List() { choice.Options[1] }); else return new ChoiceResult(new List() { choice.Options[0] }); } protected override ChoiceResult Decide_PirateShip(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // Steal coins until I have at least 3 Pirate Ship tokens on my mat 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_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_Saboteur(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_ScryingPool(Choice choice) { if (choice.PlayerSource == this) { if (choice.CardTriggers[0].Category == Category.Victory || choice.CardTriggers[0].Category == Category.Curse || choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List() { choice.Options[0] }); else return new ChoiceResult(new List() { choice.Options[1] }); } else { if (choice.CardTriggers[0].Category == Category.Victory || choice.CardTriggers[0].Category == Category.Curse || choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper) 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") return new ChoiceResult(new CardCollection(this.FindBestCardsToDiscard(choice.Cards, 2))); else // Discard all non-Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => (c.Category & Category.Treasure) != Category.Treasure))); } protected override ChoiceResult Decide_Smugglers(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } protected override ChoiceResult Decide_Spy(Choice choice) { if (choice.PlayerSource == this) { if (choice.CardTriggers[0].Category == Category.Victory || choice.CardTriggers[0].Category == Category.Curse || choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper || choice.CardTriggers[0].CardType == Cards.Hinterlands.TypeClass.Tunnel) return new ChoiceResult(new List() { choice.Options[0] }); // Discard else return new ChoiceResult(new List() { choice.Options[1] }); // Put back } else { if (choice.CardTriggers[0].Category == Category.Victory || choice.CardTriggers[0].Category == Category.Curse || choice.CardTriggers[0].CardType == Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List() { choice.Options[1] }); // Put back else return new ChoiceResult(new List() { choice.Options[0] }); // Discard } } protected override ChoiceResult Decide_Steward(Choice choice) { switch (choice.ChoiceType) { case ChoiceType.Options: // Take 2 Coins if we have at least 3 already if (this.Currency.Coin >= 3) return new ChoiceResult(new List() { choice.Options[1] }); // +2 Coins // Otherwise, just draw 2 cards else return new ChoiceResult(new List() { choice.Options[0] }); // +2 Cards case ChoiceType.Cards: // Trashing cards if (choice.Cards.Count(c => c.CardType == Cards.Universal.TypeClass.Curse) >= 2) return new ChoiceResult(new CardCollection(choice.Cards.Where(c => c.CardType == Cards.Universal.TypeClass.Curse).Take(2))); else return new ChoiceResult(new CardCollection(this.FindBestCardsToTrash(choice.Cards, 2))); default: return base.Decide_Steward(choice); } } 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")) { return new ChoiceResult(new CardCollection(choice.Cards)); } return base.Decide_Thief(choice); } protected override ChoiceResult Decide_ThroneRoom(Choice choice) { Card bestCard = this.FindBestCardToPlay(choice.Cards); if (bestCard != null) return new ChoiceResult(new CardCollection() { this.FindBestCardToPlay(choice.Cards) }); return base.Decide_ThroneRoom(choice); } 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. Random of everything else 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) { IEnumerable tournamentCCards = choice.Cards.Where(c => c.Source == Source.Cornucopia); bestCard = tournamentCCards.ElementAt(random.Next(tournamentCCards.Count())); } if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.CardType == Cards.Universal.TypeClass.Duchy); if (bestCard != null) return new ChoiceResult(new CardCollection() { bestCard }); else 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); // 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, and/or Curse 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_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) { // Always choose +2 Coin. Only choose +2 Actions if there's at least 1 Action card in hand List trustySteedChoices = new List() { choice.Options[2] }; if (this.Hand[Category.Action].Count >= 1) trustySteedChoices.Add(choice.Options[1]); else trustySteedChoices.Add(choice.Options[0]); 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")) { // 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" { // Discard 2 non-Action & non-Treasure cards at random List vaultMatchingCards = choice.Cards.Where(c => (c.Category & Category.Treasure) != Category.Treasure && (c.Category & Category.Action) != Category.Action).ToList(); Utilities.Shuffler.Shuffle(vaultMatchingCards); return new ChoiceResult(new CardCollection(vaultMatchingCards.Take(2))); } default: return base.Decide_Vault(choice); } } 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] }); else return new ChoiceResult(new List() { choice.Options[1] }); } protected override ChoiceResult Decide_WishingWell(Choice choice) { Card wwCard = null; Supply wwSupply = null; while (wwCard == null) { Type randomCard = _CardsGained[random.Next(_CardsGained.Count)]; wwSupply = choice.Supplies.FirstOrDefault(kvp => kvp.Value.CardType == randomCard).Value; if (wwSupply != null) return new ChoiceResult(wwSupply); wwCard = choice.Cards.FirstOrDefault(c => c.CardType == randomCard); if (wwCard != null) return new ChoiceResult(new CardCollection() { wwCard }); } return new ChoiceResult(new CardCollection() { wwCard }); } protected override ChoiceResult Decide_Workshop(Choice choice) { return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values, null, false)); } } }