using DominionBase.Cards; using DominionBase.Enums; using DominionBase.Piles; using DominionBase.Properties; using DominionBase.Utilities; using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; namespace DominionBase.Players.AI { public class Standard : Basic { public new static string AIName => "Standard"; public new static string AIDescription => "Baseline performance AI that makes as good of decisions as it can, but has no strong focus for buying or playing."; public static bool IsDownloading = false; // These are good starting points for creating a "learning" AI private double potionLikelihood = 1f; private double talismanLikelihood = 1f; private double castlesLikelihood = 1f; private readonly Dictionary KnownPlayerHands = new Dictionary(); public Standard(Game game, string name) : base(game, name) { } public Standard(Game game, string name, IPlayer realThis) : base(game, name, realThis) { } internal override void Setup(IGame game, IPlayer realThis) { base.Setup(game, realThis); potionLikelihood += Gaussian.NextGaussian(_Game.RNG) / 10; talismanLikelihood += Gaussian.NextGaussian(_Game.RNG) * 3 / 4; castlesLikelihood += Gaussian.NextGaussian(_Game.RNG) / 2; Console.WriteLine($"Talisman likelihood: {talismanLikelihood}"); foreach (var player in game.Players) { // Skip myself -- we already know 100% info & don't care anyway if (player == RealThis) continue; KnownPlayerHands[player.UniqueId] = new CardCollection(); player.TurnEnded += OtherPlayer_TurnEnded; player.CardsDiscarded += OtherPlayer_CardsDiscarded; player.Revealed.PileChanged += OtherPlayer_CardsRevealed_PileChanged; player.Hand.PileChanged += OtherPlayer_CardsHand_PileChanged; } } public override void EndTurn() { base.EndTurn(); RealThis.CurrentTurn?.FinishTurn(); } internal override void EndgameTriggered() { base.EndgameTriggered(); #if DEBUG if (RealThis._Game?.State == GameState.Ended) { // Check to see if Talisman was used in this game and then record how well we did to a file var talismanSupply = RealThis._Game.Table.FindSupplyPileByType(Cards.Prosperity.TypeClass.Talisman, true); if (talismanSupply != null) { var winningPoints = RealThis._Game.Winners.First().VictoryPoints; var pointPercentage = RealThis.VictoryPoints / (double)winningPoints; var weightFactor = 1 - (winningPoints - RealThis.VictoryPoints) / (double)winningPoints; while (true) { try { File.AppendAllText(Path.Combine(Application.ApplicationPath, "talisman.log"), $"Won?: {RealThis._Game.Winners.Contains(RealThis)}, Score: {RealThis.VictoryPoints}/{winningPoints}, Percentage: {pointPercentage:p}, Weight Factor: {weightFactor}, Talisman Likelihood: {talismanLikelihood}{Environment.NewLine}"); break; } catch { Thread.Sleep((new Random()).Next(250, 750)); } } } } #endif } public override void TearDown(IGame game) { base.TearDown(game); if (RealThis == null) return; foreach (var player in RealThis._Game.Players) { if (player == RealThis) continue; player.TurnEnded -= OtherPlayer_TurnEnded; player.CardsDiscarded -= OtherPlayer_CardsDiscarded; player.Revealed.PileChanged -= OtherPlayer_CardsRevealed_PileChanged; player.Hand.PileChanged -= OtherPlayer_CardsHand_PileChanged; } } private void OtherPlayer_TurnEnded(object sender, TurnEndedEventArgs e) { KnownPlayerHands[e.Player.UniqueId] = new CardCollection(); } private void OtherPlayer_CardsRevealed_PileChanged(object sender, PileChangedEventArgs e) { KnownPlayerHands[e.Player.PlayerUniqueId].AddRange(e.AddedCards.OfType()); } private void OtherPlayer_CardsHand_PileChanged(object sender, PileChangedEventArgs e) { switch (e.OperationPerformed) { case Operation.Added: // We can add red-backed cards (Stash) to the known cards when added to a player's hand KnownPlayerHands[e.Player.PlayerUniqueId].AddRange(e.AddedCards.OfType().Where(c => c.CardBack == CardBack.Red)); break; case Operation.Removed: foreach (var card in e.RemovedCards.OfType().Where(c => c.CardBack == CardBack.Red)) { if (KnownPlayerHands[e.Player.PlayerUniqueId].Contains(card)) KnownPlayerHands[e.Player.PlayerUniqueId].Remove(card); } break; } } private void OtherPlayer_CardsDiscarded(object sender, CardsDiscardEventArgs e) { if (!KnownPlayerHands.ContainsKey(((Player)sender).UniqueId)) return; // We shouldn't cheat -- only the last card should be visible var lastCard = e.Cards.LastOrDefault(); if (lastCard == null) return; // Remove the card if it's found in our list of cards that we know about var foundCard = KnownPlayerHands[((Player)sender).UniqueId].FirstOrDefault(c => c.Name == lastCard.Name); if (foundCard != null) KnownPlayerHands[((Player)sender).UniqueId].Remove(foundCard); } protected override void PlayTreasure() { if ((RealThis.Phase == PhaseEnum.ActionTreasure || RealThis.Phase == PhaseEnum.BuyTreasure) && RealThis.PlayerMode == PlayerMode.Normal) { Thread.Sleep(SleepTime); var nextTreasures = FindBestCardsToPlay(RealThis.Hand[Categories.Treasure]); if (nextTreasures.Any()) { RealThis.PlayCards(nextTreasures); } else if (RealThis.TokenPiles[Cards.Guilds.TypeClass.Coffer].Any() && RealThis.Phase == PhaseEnum.BuyTreasure) { var coinTokens = RealThis.TokenPiles[Cards.Guilds.TypeClass.Coffer].Count; var currentCurrency = RealThis.Currency.Coin.Value; var colonyGainable = _Game.Table.TableEntities.ContainsKey(Cards.Prosperity.TypeClass.Colony) && ((ISupply)_Game.Table.TableEntities[Cards.Prosperity.TypeClass.Colony]).CanBuy(RealThis, new Currency(((ISupply)_Game.Table.TableEntities[Cards.Prosperity.TypeClass.Colony]).CurrentCost)); var provinceGainable = _Game.Table.Province.CanBuy(RealThis, new Currency(_Game.Table.Province.CurrentCost)); var spendCoinTokens = 0; // Make slightly better decisions about spending coins vs. not // If we can gain a Colony by spending 1 coin, do so! if (currentCurrency == 10 && colonyGainable) spendCoinTokens = 1; // Less excitingly, if we can gain a Province by spending 1 coin, do so! else if (currentCurrency == 7 && provinceGainable) spendCoinTokens = 1; // If it's getting later in the game and we have enough Coin tokens to get a Colony, DO IT! else if (GameProgressLeft < 0.75 && colonyGainable && currentCurrency + coinTokens >= ((ISupply)_Game.Table.TableEntities[Cards.Prosperity.TypeClass.Colony]).CurrentCost.Coin.Value) spendCoinTokens = ((ISupply)_Game.Table.TableEntities[Cards.Prosperity.TypeClass.Colony]).CurrentCost.Coin.Value - currentCurrency; // If it's getting later in the game and we have enough Coin tokens to get a Province, DO IT! else if (GameProgressLeft < 0.50 && provinceGainable && currentCurrency + coinTokens >= _Game.Table.Province.CurrentCost.Coin.Value) spendCoinTokens = _Game.Table.Province.CurrentCost.Coin.Value - currentCurrency; if (spendCoinTokens == 0) { var previousBestScore = -1.0; var buyables = new List(); for (var coinTokenCount = 0; coinTokenCount <= coinTokens; coinTokenCount++) { buyables.Clear(); buyables.AddRange(RealThis._Game.Table.TableEntities.Values.OfType().Where(supply => supply.CanBuy(RealThis, RealThis.Currency + new Currencies.Coin(coinTokenCount)) && ShouldBuy(supply))); var scores = ValuateCardsToBuy(buyables); var bestScore = scores.Keys.OrderByDescending(k => k).FirstOrDefault(); if (bestScore > 0 && (previousBestScore < 0 || bestScore > (coinTokenCount - spendCoinTokens + 1) + previousBestScore)) { spendCoinTokens = coinTokenCount; previousBestScore = bestScore; } } } if (spendCoinTokens > 0) RealThis.PlayTokens(_Game, Cards.Guilds.TypeClass.Coffer, spendCoinTokens); // Otherwise, we'll just save them for now. This isn't great, but it's better than a kick to the head. if (RealThis.Phase != PhaseEnum.Buy) { // Always pay off Debt tokens if able var debtsToPayOff = Math.Min(RealThis.TokenPiles[Cards.Empires.TypeClass.DebtToken].Count, RealThis.Currency.Coin.Value); if (debtsToPayOff > 0) RealThis.PlayTokens(_Game, Cards.Empires.TypeClass.DebtToken, debtsToPayOff); RealThis.GoToNextPhase(); } } else { // Always pay off Debt tokens if able var debtsToPayOff = Math.Min(RealThis.TokenPiles[Cards.Empires.TypeClass.DebtToken].Count, RealThis.Currency.Coin.Value); if (debtsToPayOff > 0) RealThis.PlayTokens(_Game, Cards.Empires.TypeClass.DebtToken, debtsToPayOff); RealThis.GoToNextPhase(); } } } private double ValueOfCardToPlay(ICard card) { double multiplier = 1.0; double value = (card.BaseCost.Coin.Value + 2.5 * card.BaseCost.Potion.Value + 0.9 * card.BaseCost.Debt.Value); if (card.Category.HasFlag(Categories.Prize)) value = 7; else if (card.Category.HasFlag(Categories.Spirit)) multiplier = 1.2; else if (card is Cards.Prosperity.GrandMarket || card is Cards.Prosperity2ndEdition.GrandMarket) value = 6.5; else if (card is Cards.Prosperity.Peddler) value = 5.5; else if (card is Cards.DarkAges.Madman || card is Cards.DarkAges2ndEdition.Madman) value = 4; else if (card is Cards.DarkAges.Mercenary || card is Cards.DarkAges2ndEdition.Mercenary) value = 4; else if (card is Cards.Guilds.Doctor || card is Cards.Guilds2ndEdition.Doctor) value = 2.5; else if (card is Cards.Guilds.Herald || card is Cards.Guilds2ndEdition.Herald) value = 4.5; else if (card is Cards.Guilds.Stonemason || card is Cards.Guilds2ndEdition.Stonemason) value = 1.5; else if (card is Cards.Nocturne.WillOWisp) value = 2.5; else if (card is Cards.Nocturne.Wish) value = 7; else if (card is Cards.Nocturne.Fool && RealThis.Takeables.Any(s => s is Cards.Nocturne.LostInTheWoods)) value = 0; else if (card is Cards.Nocturne.Leprechaun) { if (RealThis.InPlayAndSetAside.Count == 6) value = 6; else value = 2.2; } else if (card is Cards.Renaissance.Swashbuckler) { if (RealThis.DrawPile.Count >= 3) { if (RealThis.Coffers >= 3) value = 6; else value = 5.5; } else value = 4.5; } else if (card is Cards.Menagerie.AnimalFair) value = 5.5 + RealThis._Game.Table.EmptySupplyPiles / 2d; else if (card is Cards.Menagerie.Cavalry) value = 2; else if (card is Cards.Menagerie.Destrier) value = 5; else if (card is Cards.Menagerie.Fisherman) value = 3.5; // Turns into Village after it's in our deck else if (card is Cards.Menagerie.Hostelry) value = 3; else if (card is Cards.Menagerie.HuntingLodge) { var totalHandValue = RealThis.Hand.Sum(c => ComputeValueInDeck(c)); var totalDeckValue = RealThis.DrawPile.LookThrough(c => true).Sum(c => ComputeValueInDeck(c)) + RealThis.DiscardPile.LookThrough(c => true).Sum(c => ComputeValueInDeck(c)); if (totalHandValue / RealThis.Hand.Count > totalDeckValue / (RealThis.DrawPile.Count + RealThis.DiscardPile.LookThrough(c => true).Count)) value = 3; else value = 6; } else if (card is Cards.Menagerie.Paddock) value = 3.5 + RealThis._Game.Table.EmptySupplyPiles; else if (card is Cards.Menagerie.Wayfarer) value = 4.25; return multiplier * value; } protected override Card FindBestCardToPlay(IEnumerable cards) { // Ignore cards we don't want to play and sort the cards by ValueOfCardToPlay cards = cards.Where(ShouldPlay).OrderByDescending(ValueOfCardToPlay).ToList(); // Always play King's Court if there is one (?) var kc = cards.FirstOrDefault(card => card is Cards.Prosperity.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 is Cards.Base.Chapel || c is Cards.Base.Library || c is Cards.Base.Remodel || c is Cards.Base2ndEdition.Library || c is Cards.Base2ndEdition.Remodel || c is Cards.Intrigue.SecretChamber || c is Cards.Intrigue.Upgrade || c is Cards.Seaside.Island || c is Cards.Seaside.Lookout || c is Cards.Seaside.Outpost || c is Cards.Seaside.Salvager || c is Cards.Seaside.Tactician || c is Cards.Seaside.TreasureMap || c is Cards.Seaside2ndEdition.Island || c is Cards.Seaside2ndEdition.Lookout || c is Cards.Seaside2ndEdition.Outpost || c is Cards.Seaside2ndEdition.Salvager || c is Cards.Seaside2ndEdition.Tactician || c is Cards.Seaside2ndEdition.TreasureMap || c is Cards.Prosperity.CountingHouse || c is Cards.Prosperity.Forge || c is Cards.Prosperity.TradeRoute || c is Cards.Prosperity.Watchtower || c is Cards.Prosperity2ndEdition.CountingHouse || c is Cards.Prosperity2ndEdition.Forge || c is Cards.Prosperity2ndEdition.TradeRoute || c is Cards.Prosperity2ndEdition.Watchtower || c is Cards.Cornucopia.Remake || c is Cards.Cornucopia2ndEdition.Remake || c is Cards.Hinterlands.Develop || c is Cards.Hinterlands2ndEdition.Develop || c is Cards.DarkAges.JunkDealer || c is Cards.DarkAges.Procession || c is Cards.DarkAges.Rats || c is Cards.DarkAges.Rebuild || c is Cards.DarkAges2ndEdition.Rats || c is Cards.DarkAges2ndEdition.Rebuild || c is Cards.DarkAges2019Errata.Procession || c is Cards.Guilds.MerchantGuild || c is Cards.Guilds.Stonemason || c is Cards.Guilds2ndEdition.MerchantGuild || c is Cards.Guilds2ndEdition.Stonemason || c is Cards.Adventures.DistantLands || c is Cards.Adventures.Duplicate || c is Cards.Adventures.Champion || c is Cards.Adventures.Teacher || c is Cards.Adventures.RoyalCarriage || c is Cards.Adventures2ndEdition.Champion || c is Cards.Adventures2ndEdition.RoyalCarriage || c is Cards.Empires.Sacrifice || c is Cards.Promotional.Prince || c is Cards.Nocturne.ZombieApprentice || c is Cards.Nocturne.ZombieMason || c is Cards.Renaissance.Hideout || c is Cards.Renaissance.Priest || c is Cards.Renaissance.Recruiter || c is Cards.Renaissance.Research || c is Cards.Menagerie.Scrap ) ) return kc; } // Always play Throne Room if there is one (?) var tr = cards.FirstOrDefault(card => card is Cards.Base.ThroneRoom || card is Cards.Base2ndEdition.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 is Cards.Base.Chapel || c is Cards.Base.Library || c is Cards.Base.Remodel || c is Cards.Base2ndEdition.Library || c is Cards.Base2ndEdition.Remodel || c is Cards.Intrigue.SecretChamber || c is Cards.Intrigue.Upgrade || c is Cards.Seaside.Island || c is Cards.Seaside.Lookout || c is Cards.Seaside.Outpost || c is Cards.Seaside.Salvager || c is Cards.Seaside.Tactician || c is Cards.Seaside.TreasureMap || c is Cards.Seaside2ndEdition.Island || c is Cards.Seaside2ndEdition.Lookout || c is Cards.Seaside2ndEdition.Outpost || c is Cards.Seaside2ndEdition.Salvager || c is Cards.Seaside2ndEdition.Tactician || c is Cards.Seaside2ndEdition.TreasureMap || c is Cards.Prosperity.CountingHouse || c is Cards.Prosperity.Forge || c is Cards.Prosperity.TradeRoute || c is Cards.Prosperity.Watchtower || c is Cards.Prosperity2ndEdition.CountingHouse || c is Cards.Prosperity2ndEdition.Forge || c is Cards.Prosperity2ndEdition.TradeRoute || c is Cards.Prosperity2ndEdition.Watchtower || c is Cards.Cornucopia.Remake || c is Cards.Cornucopia2ndEdition.Remake || c is Cards.Hinterlands.Develop || c is Cards.Hinterlands2ndEdition.Develop || c is Cards.DarkAges.JunkDealer || c is Cards.DarkAges.Procession || c is Cards.DarkAges.Rats || c is Cards.DarkAges.Rebuild || c is Cards.DarkAges2ndEdition.Rats || c is Cards.DarkAges2ndEdition.Rebuild || c is Cards.DarkAges2019Errata.Procession || c is Cards.Guilds.MerchantGuild || c is Cards.Guilds.Stonemason || c is Cards.Guilds2ndEdition.MerchantGuild || c is Cards.Guilds2ndEdition.Stonemason || c is Cards.Adventures.DistantLands || c is Cards.Adventures.Duplicate || c is Cards.Adventures.Champion || c is Cards.Adventures.Teacher || c is Cards.Adventures.RoyalCarriage || c is Cards.Adventures2ndEdition.Champion || c is Cards.Adventures2ndEdition.RoyalCarriage || c is Cards.Empires.Sacrifice || c is Cards.Promotional.Prince || c is Cards.Nocturne.ZombieApprentice || c is Cards.Nocturne.ZombieMason || c is Cards.Renaissance.Hideout || c is Cards.Renaissance.Priest || c is Cards.Renaissance.Recruiter || c is Cards.Renaissance.Research || c is Cards.Menagerie.Scrap )) return tr; } // Always play Disciple if there is one (?) var disciple = cards.FirstOrDefault(card => card is Cards.Adventures.Disciple); if (disciple != null) { // Not quite -- Don't play Disciple 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 Disciple'ing certain cards like Island if (!cards.All(c => c is Cards.Base.Chapel || c is Cards.Base.Library || c is Cards.Base.Remodel || c is Cards.Base2ndEdition.Library || c is Cards.Base2ndEdition.Remodel || c is Cards.Intrigue.SecretChamber || c is Cards.Intrigue.Upgrade || c is Cards.Seaside.Island || c is Cards.Seaside.Lookout || c is Cards.Seaside.Outpost || c is Cards.Seaside.Salvager || c is Cards.Seaside.Tactician || c is Cards.Seaside.TreasureMap || c is Cards.Seaside2ndEdition.Island || c is Cards.Seaside2ndEdition.Lookout || c is Cards.Seaside2ndEdition.Outpost || c is Cards.Seaside2ndEdition.Salvager || c is Cards.Seaside2ndEdition.Tactician || c is Cards.Seaside2ndEdition.TreasureMap || c is Cards.Prosperity.CountingHouse || c is Cards.Prosperity.Forge || c is Cards.Prosperity.TradeRoute || c is Cards.Prosperity.Watchtower || c is Cards.Prosperity2ndEdition.CountingHouse || c is Cards.Prosperity2ndEdition.Forge || c is Cards.Prosperity2ndEdition.TradeRoute || c is Cards.Prosperity2ndEdition.Watchtower || c is Cards.Cornucopia.Remake || c is Cards.Cornucopia2ndEdition.Remake || c is Cards.Hinterlands.Develop || c is Cards.Hinterlands2ndEdition.Develop || c is Cards.DarkAges.JunkDealer || c is Cards.DarkAges.Procession || c is Cards.DarkAges.Rats || c is Cards.DarkAges.Rebuild || c is Cards.DarkAges2ndEdition.Rats || c is Cards.DarkAges2ndEdition.Rebuild || c is Cards.DarkAges2019Errata.Procession || c is Cards.Guilds.MerchantGuild || c is Cards.Guilds.Stonemason || c is Cards.Guilds2ndEdition.MerchantGuild || c is Cards.Guilds2ndEdition.Stonemason || c is Cards.Adventures.DistantLands || c is Cards.Adventures.Duplicate || c is Cards.Adventures.Champion || c is Cards.Adventures.Teacher || c is Cards.Adventures.RoyalCarriage || c is Cards.Adventures2ndEdition.Champion || c is Cards.Adventures2ndEdition.RoyalCarriage || c is Cards.Empires.Sacrifice || c is Cards.Promotional.Prince || c is Cards.Nocturne.ZombieApprentice || c is Cards.Nocturne.ZombieMason || c is Cards.Renaissance.Hideout || c is Cards.Renaissance.Priest || c is Cards.Renaissance.Recruiter || c is Cards.Renaissance.Research || c is Cards.Menagerie.Scrap )) return disciple; } // Only always play Crown if there's at least 1 Action card we want to play var crown = cards.FirstOrDefault(card => card is Cards.Empires.Crown); if (crown != null) { // Not quite -- Don't play Crown 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 Crown'ing certain cards like Island var playableActionCards = cards.Where(c => !(c is Cards.Base.Chapel || c is Cards.Base.Library || c is Cards.Base.Remodel || c is Cards.Base2ndEdition.Library || c is Cards.Base2ndEdition.Remodel || c is Cards.Intrigue.SecretChamber || c is Cards.Intrigue.Upgrade || c is Cards.Seaside.Island || c is Cards.Seaside.Lookout || c is Cards.Seaside.Outpost || c is Cards.Seaside.Salvager || c is Cards.Seaside.Tactician || c is Cards.Seaside.TreasureMap || c is Cards.Seaside2ndEdition.Island || c is Cards.Seaside2ndEdition.Lookout || c is Cards.Seaside2ndEdition.Outpost || c is Cards.Seaside2ndEdition.Salvager || c is Cards.Seaside2ndEdition.Tactician || c is Cards.Seaside2ndEdition.TreasureMap || c is Cards.Prosperity.CountingHouse || c is Cards.Prosperity.Forge || c is Cards.Prosperity.TradeRoute || c is Cards.Prosperity.Watchtower || c is Cards.Prosperity2ndEdition.CountingHouse || c is Cards.Prosperity2ndEdition.Forge || c is Cards.Prosperity2ndEdition.TradeRoute || c is Cards.Prosperity2ndEdition.Watchtower || c is Cards.Cornucopia.Remake || c is Cards.Cornucopia2ndEdition.Remake || c is Cards.Hinterlands.Develop || c is Cards.Hinterlands2ndEdition.Develop || c is Cards.DarkAges.JunkDealer || c is Cards.DarkAges.Procession || c is Cards.DarkAges.Rats || c is Cards.DarkAges.Rebuild || c is Cards.DarkAges2ndEdition.Rats || c is Cards.DarkAges2ndEdition.Rebuild || c is Cards.DarkAges2019Errata.Procession || c is Cards.Guilds.MerchantGuild || c is Cards.Guilds.Stonemason || c is Cards.Guilds2ndEdition.MerchantGuild || c is Cards.Guilds2ndEdition.Stonemason || c is Cards.Adventures.DistantLands || c is Cards.Adventures.Duplicate || c is Cards.Adventures.Champion || c is Cards.Adventures.Teacher || c is Cards.Adventures.RoyalCarriage || c is Cards.Adventures2ndEdition.Champion || c is Cards.Adventures2ndEdition.RoyalCarriage || c is Cards.Empires.Sacrifice || c is Cards.Promotional.Prince || c is Cards.Nocturne.ZombieApprentice || c is Cards.Nocturne.ZombieMason || c is Cards.Renaissance.Hideout || c is Cards.Renaissance.Priest || c is Cards.Renaissance.Recruiter || c is Cards.Renaissance.Research || c is Cards.Menagerie.Scrap )); if (playableActionCards.Any()) return crown; } // Play Menagerie first if we've got a hand with only unique cards if (cards.Count(c => c is Cards.Cornucopia.Menagerie || c is Cards.Cornucopia2ndEdition.Menagerie) == 1) { var typesMenagerie = RealThis.Hand.Select(c => c.Type); if (typesMenagerie.Count() == typesMenagerie.Distinct().Count()) return cards.First(c => c is Cards.Cornucopia.Menagerie || c is Cards.Cornucopia2ndEdition.Menagerie); } // Always play Imp or Conclave if we have an Action card we want to play in our hand that isn't in play var impConc = cards.FirstOrDefault(c => c is Cards.Nocturne.Imp || c is Cards.Nocturne.Conclave); if (impConc != null) { if (cards.Any(c => c.Category.HasFlag(Categories.Action) && !(c.Type != impConc.Type) && !RealThis.InPlayAndSetAside.Any(ip => ip.Type == c.Type) )) return impConc; } // Keep Shanty Town available to play only if any the following criteria are satisfied if (cards.Any(c => c is Cards.Intrigue.ShantyTown || c is Cards.Intrigue2ndEdition.ShantyTown)) { if (!cards.Any(c => c.Type != Cards.Intrigue.TypeClass.ShantyTown && c.Type != Cards.Intrigue2ndEdition.TypeClass.ShantyTown) || // No other Action cards in hand cards.Count(c => c is Cards.Intrigue.ShantyTown || c is Cards.Intrigue2ndEdition.ShantyTown) > 1 || // At least 1 other Shanty Town in hand (ActionsAvailable() == 1 && cards.Count(c => ShouldPlay(c) && c.Benefit.Actions == 0) >= 2) || // 1 Action left & 2 or more Terminal Actions (ActionsAvailable() == 1 && cards.Any(c => ShouldPlay(c) && c.Benefit.Actions == 0 && c.Benefit.Cards > 0)) || // 1 Action left & 1 or more card-drawing Actions RealThis.Hand[c => c is Cards.Cornucopia.HornOfPlenty || c is Cards.Cornucopia2ndEdition.HornOfPlenty].Any()) // Horn of Plenty in hand { // Keep it. Criteria has been satisfied } else cards = cards.Where(c => c.Type != Cards.Intrigue.TypeClass.ShantyTown && c.Type != Cards.Intrigue2ndEdition.TypeClass.ShantyTown); } // This logic only applies if we've not played Snowy Village yet this turn. It should come last in the cantrip/village chain // and makes us rethink what Action cards we should play after it if (!RealThis.CurrentTurn.CardsPlayed.Any(c => c is Cards.Menagerie.SnowyVillage)) { Card plusActions = null; // Villages first (except Snowy Village) if (plusActions == null) plusActions = cards.FirstOrDefault(card => card.Benefit.Actions > 1 && !(card is Cards.Menagerie.SnowyVillage)); // Then Conspirator if (plusActions == null && RealThis.CurrentTurn.CardsPlayed.Count(c => c.Category.HasFlag(Categories.Action)) >= 2) plusActions = cards.FirstOrDefault(c => c is Cards.Intrigue.Conspirator || c is Cards.Intrigue2ndEdition.Conspirator); // Then Action cards that don't eat Actions (except Snowy Village) if (plusActions == null) plusActions = cards.FirstOrDefault(card => card.Benefit.Actions > 0 && !(card is Cards.Menagerie.SnowyVillage)); // Then Snowy Village if (plusActions == null) plusActions = cards.FirstOrDefault(card => card is Cards.Menagerie.SnowyVillage); if (plusActions != null) return plusActions; } Turn previousTurn = null; if (RealThis._Game.TurnsTaken.Count > 1) previousTurn = RealThis._Game.TurnsTaken[RealThis._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 is Cards.Seaside.Smugglers || card is Cards.Seaside2ndEdition.Smugglers) && previousTurn != null && previousTurn.CardsGained.Any(card => _Game.ComputeCost(card).Coin >= 5 && _Game.ComputeCost(card).Potion == 0 && _Game.ComputeCost(card).Debt == 0 && RealThis._Game.Table.TableEntities.ContainsKey(card) && ((ISupply)RealThis._Game.Table.TableEntities[card]).CanGain(card.Type)) && _Game.RNG.Next(0, 5) < 2) return cards.First(card => card is Cards.Seaside.Smugglers || card is Cards.Seaside2ndEdition.Smugglers); if (RealThis.Hand[Categories.Curse].Any()) { // Play an Ambassador card if there is one and we have at least 1 Curse in our hand var trasher = cards.FirstOrDefault( card => card is Cards.Base.Chapel || card is Cards.Base.Remodel || card is Cards.Base2ndEdition.Remodel || (card is Cards.Intrigue.TradingPost && RealThis.Hand[Categories.Curse].Count > 1) || card is Cards.Intrigue.Upgrade || (card is Cards.Intrigue2ndEdition.TradingPost && RealThis.Hand[Categories.Curse].Count > 1) || card is Cards.Seaside.Ambassador || card is Cards.Seaside.Salvager || card is Cards.Seaside2ndEdition.Ambassador || card is Cards.Seaside2ndEdition.Salvager || card is Cards.Alchemy.Apprentice || card is Cards.Alchemy2ndEdition.Apprentice || card is Cards.Prosperity.Expand || card is Cards.Prosperity.Forge || card is Cards.Prosperity.TradeRoute || card is Cards.Prosperity2ndEdition.Expand || card is Cards.Prosperity2ndEdition.Forge || card is Cards.Prosperity2ndEdition.TradeRoute || (card is Cards.Cornucopia.Remake && RealThis.Hand[Categories.Curse].Count > 1) || (card is Cards.Cornucopia2ndEdition.Remake && RealThis.Hand[Categories.Curse].Count > 1) || card is Cards.Hinterlands.Develop || card is Cards.Hinterlands.JackOfAllTrades || card is Cards.Hinterlands.Trader || card is Cards.Hinterlands2ndEdition.Develop || card is Cards.Hinterlands2ndEdition.JackOfAllTrades || card is Cards.Hinterlands2ndEdition.Trader || card is Cards.DarkAges.JunkDealer || card is Cards.DarkAges.Rats || card is Cards.DarkAges2ndEdition.Rats || card is Cards.Guilds.Butcher || card is Cards.Guilds.Stonemason || card is Cards.Guilds2ndEdition.Butcher || card is Cards.Guilds2ndEdition.Stonemason || card is Cards.Adventures.Amulet || card is Cards.Adventures.Raze || card is Cards.Adventures2ndEdition.Raze || card is Cards.Empires.Catapult || card is Cards.Empires.Sacrifice || card is Cards.Empires.Temple || card is Cards.Nocturne.Bat || card is Cards.Nocturne.Exorcist || card is Cards.Nocturne.Goat || card is Cards.Renaissance.Hideout || card is Cards.Renaissance.Priest || card is Cards.Renaissance.Recruiter || card is Cards.Renaissance.Research || card is Cards.Menagerie.Scrap ); 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.Any(c => c is Cards.Hinterlands.Trader)) { if (GameProgressLeft > 0.75 && (RealThis.Hand[Cards.Universal.TypeClass.Copper].Count == 0 || RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure) && c.Type != Cards.Universal.TypeClass.Copper) < 3)) cards = cards.Where(c => !(c is Cards.Hinterlands.Trader) && !(c is Cards.Hinterlands2ndEdition.Trader)); else if (!RealThis.Hand.Any(c => !c.Category.HasFlag(Categories.Victory) && _Game.ComputeCost(c).Coin.Value < 3)) cards = cards.Where(c => !(c is Cards.Hinterlands.Trader) && !(c is Cards.Hinterlands2ndEdition.Trader)); } // Don't play Courtyard or Dungeon if there are fewer than 2 cards to draw if (cards.Any(c => c is Cards.Intrigue.Courtyard || c is Cards.Intrigue2ndEdition.Courtyard || c is Cards.Adventures.Dungeon || c is Cards.Adventures2ndEdition.Dungeon) && RealThis.CountAll(RealThis, c => true, onlyCurrentlyDrawable: true) < 2) cards = cards.Where(c => !(c is Cards.Intrigue.Courtyard) && !(c is Cards.Intrigue2ndEdition.Courtyard) && !(c is Cards.Adventures.Dungeon) && !(c is Cards.Adventures2ndEdition.Dungeon)); // Don't bother with Pooka if we don't have a Treasure we want to trash if (cards.Any(c => c is Cards.Nocturne.Pooka)) { Predicate trashableTreasuresPredicate = c => c is Cards.Universal.Copper || c is Cards.Prosperity.Loan || c is Cards.Prosperity.Quarry || c is Cards.Prosperity2ndEdition.Loan || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece || c is Cards.Adventures.CoinOfTheRealm || c is Cards.Adventures2ndEdition.CoinOfTheRealm || c is Cards.Empires.Rocks || c is Cards.Nocturne.HauntedMirror || c is Cards.Nocturne.MagicLamp || c is Cards.Nocturne.Goat || c is Cards.Nocturne.LuckyCoin; var trashableTreasures = RealThis.CountAll(RealThis, trashableTreasuresPredicate); var treasureDensity = ComputeAverageCoinValueInDeck(); var ratio = trashableTreasures / treasureDensity; Console.WriteLine($"Pooka treasure density ratio: {ratio} -- {trashableTreasures} : {treasureDensity}"); if (!RealThis.Hand[trashableTreasuresPredicate].Any() || (ratio > 0.5 && _Game.TurnsTaken.Count(t => t.Player == RealThis) > 5)) cards = cards.Where(c => !(c is Cards.Nocturne.Pooka)); } // Only ever play Tactician if our hand is "crap" if (cards.Any(c => c is Cards.Seaside.Tactician || c is Cards.Seaside2ndEdition.Tactician)) { var playableTerminals = cards.Count(c => c is Cards.Seaside.Tactician || c is Cards.Seaside2ndEdition.Tactician || (c.Category.HasFlag(Categories.Action) && ShouldPlay(c) && c.Traits.HasFlag(Traits.Terminal)) ) - 1; // Excluding the Tactician we're looking at var villages = cards.Count(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c) && c.Traits.HasFlag(Traits.PlusMultipleActions)); var junk = cards.Count(c => !ShouldPlay(c)) + RealThis.Hand.Count(c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins) || (c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Treasure) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Night)) ); var deadCards = Math.Max(0, ActionsAvailable() + villages - playableTerminals) + junk; var deadCardRatio = (double)deadCards / RealThis.Hand.Count; var bestCard = FindBestCardToBuy(RealThis._Game.Table.TableEntities.Values.OfType().Where( buyable => !buyable.Category.HasFlag(Categories.Event) // Events screw up this logic a fair amount && buyable.CanBuy(RealThis) ).ToList()); var skipTactician = false; if (!(deadCardRatio >= 0.5 || deadCardRatio >= 0.3 && bestCard != null || RealThis.Hand.Count < 6)) { skipTactician = true; cards = cards.Where(c => !(c is Cards.Seaside.Tactician || c is Cards.Seaside2ndEdition.Tactician)); } Console.WriteLine($"For {RealThis} ---- Tactician values {ActionsAvailable()}, {villages}, {playableTerminals}, {junk}, {deadCardRatio}, {skipTactician} --- {RealThis.Hand}"); Console.WriteLine($"For {RealThis} ---- {bestCard}"); } if (cards.Any()) // Just play the most expensive one return cards.ElementAt(0); return null; } protected override CardCollection FindBestCardsToPlay(IEnumerable cards) { // Play Crown first var cardsList = cards as IList ?? cards.ToList(); cardsList = cardsList.Where(ShouldPlay).ToList(); var crown = cardsList.FirstOrDefault(c => c is Cards.Empires.Crown); if (crown != null) return new CardCollection { crown }; // Play all Contrabands next var contraband = cardsList.FirstOrDefault(c => c is Cards.Prosperity.Contraband); if (contraband != null) return new CardCollection { contraband }; // Play all Counterfeits next var counterfeit = cardsList.FirstOrDefault(c => c is Cards.DarkAges.Counterfeit); if (counterfeit != null) return new CardCollection { counterfeit }; // Play all normal treasures next var normalTreasures = cardsList.Where( c => c.Category.HasFlag(Categories.Treasure) && !( c is Cards.Prosperity.Loan || c is Cards.Prosperity.Venture || c is Cards.Prosperity2ndEdition.Loan || c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains || c is Cards.Cornucopia.HornOfPlenty || c is Cards.Cornucopia2ndEdition.HornOfPlenty || c is Cards.Prosperity.Bank || c is Cards.Empires.Fortune )).ToList(); if (normalTreasures.Any()) 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. var loanVenture = cardsList.Where(c => c is Cards.Prosperity.Loan || c is Cards.Prosperity.Venture || c is Cards.Prosperity2ndEdition.Loan).ToList(); if (loanVenture.Any()) return new CardCollection(loanVenture); // Play Ill-Gotten Gains later so we can figure out if we need that extra Copper var illGottenGains = cardsList.FirstOrDefault(c => c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains); if (illGottenGains != null) return new CardCollection { illGottenGains }; // Always play Bank & Horn of Plenty last var bankHornOfPlenty = cardsList.Where(c => c is Cards.Prosperity.Bank || c is Cards.Cornucopia.HornOfPlenty || c is Cards.Cornucopia2ndEdition.HornOfPlenty); foreach (var card in bankHornOfPlenty) { if (card is Cards.Cornucopia.HornOfPlenty || card is Cards.Cornucopia2ndEdition.HornOfPlenty) { IList types = new List { Cards.Cornucopia.TypeClass.HornOfPlenty, Cards.Cornucopia2ndEdition.TypeClass.HornOfPlenty }; foreach (var c in RealThis.InPlay) { var t = c.Type; if (!types.Contains(t)) types.Add(t); } foreach (var c in RealThis.SetAside) { var t = c.Type; if (!types.Contains(t)) types.Add(t); } // Don't even bother playing Horn of Plenty if there's nothing for me to gain except Copper & Estate if (RealThis._Game.Table.TableEntities.Values.OfType().FirstOrDefault( s => s.Type != Cards.Universal.TypeClass.Copper && s.Type != Cards.Universal.TypeClass.Estate && s.CanGain() && ShouldBuy(s) && s.CurrentCost <= new Cost(types.Count)) == null) continue; } return new CardCollection { card }; } // Play Fortune last var fortune = cardsList.FirstOrDefault(c => c is Cards.Empires.Fortune); if (fortune != null) return new CardCollection { fortune }; // 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; if (type == Cards.Base.TypeClass.Moneylender) return false; if (type == Cards.Base.TypeClass.Chapel) return false; if (type == Cards.Base.TypeClass.Remodel) return false; if (type == Cards.Base2ndEdition.TypeClass.Moneylender) return false; if (type == Cards.Base2ndEdition.TypeClass.Remodel) return false; if (type == Cards.Intrigue.TypeClass.Upgrade) return false; if (type == Cards.Intrigue.TypeClass.TradingPost) return false; if (type == Cards.Intrigue.TypeClass.Masquerade) return false; if (type == Cards.Intrigue.TypeClass.Coppersmith) return false; if (type == Cards.Intrigue2ndEdition.TypeClass.Masquerade) return false; if (type == Cards.Intrigue2ndEdition.TypeClass.TradingPost) return false; if (type == Cards.Seaside.TypeClass.Lookout) return false; if (type == Cards.Seaside.TypeClass.Ambassador) return false; if (type == Cards.Seaside.TypeClass.Navigator) return false; if (type == Cards.Seaside.TypeClass.Salvager) return false; if (type == Cards.Seaside.TypeClass.TreasureMap) return false; if (type == Cards.Seaside2ndEdition.TypeClass.Lookout) return false; if (type == Cards.Seaside2ndEdition.TypeClass.Ambassador) return false; if (type == Cards.Seaside2ndEdition.TypeClass.Navigator) return false; if (type == Cards.Seaside2ndEdition.TypeClass.Salvager) return false; if (type == Cards.Seaside2ndEdition.TypeClass.TreasureMap) return false; //if (type == Cards.Alchemy.TypeClass.Potion) // return false; //if (type == Cards.Alchemy.TypeClass.Possession) // return false; if (type == Cards.Alchemy.TypeClass.Apprentice) return false; if (type == Cards.Alchemy.TypeClass.Transmute) return false; if (type == Cards.Alchemy2ndEdition.TypeClass.Apprentice) return false; if (type == Cards.Prosperity.TypeClass.Expand) return false; if (type == Cards.Prosperity.TypeClass.Forge) return false; if (type == Cards.Prosperity.TypeClass.TradeRoute) return false; if (type == Cards.Prosperity2ndEdition.TypeClass.Expand) return false; if (type == Cards.Prosperity2ndEdition.TypeClass.Forge) return false; if (type == Cards.Prosperity2ndEdition.TypeClass.TradeRoute) return false; if (type == Cards.Cornucopia.TypeClass.Remake) return false; if (type == Cards.Cornucopia2ndEdition.TypeClass.Remake) return false; if (type == Cards.Hinterlands.TypeClass.Develop) return false; if (type == Cards.Hinterlands2ndEdition.TypeClass.Develop) return false; if (type == Cards.DarkAges.TypeClass.BandOfMisfits) return false; if (type == Cards.DarkAges.TypeClass.Forager) return false; if (type == Cards.DarkAges.TypeClass.Procession) return false; if (type == Cards.DarkAges.TypeClass.Rats) return false; if (type == Cards.DarkAges.TypeClass.Rebuild) return false; if (type == Cards.DarkAges.TypeClass.RuinsSupply || type == Cards.DarkAges.TypeClass.AbandonedMine || type == Cards.DarkAges.TypeClass.RuinedLibrary || type == Cards.DarkAges.TypeClass.RuinedMarket || type == Cards.DarkAges.TypeClass.RuinedVillage || type == Cards.DarkAges.TypeClass.Survivors) return false; if (type == Cards.DarkAges2ndEdition.TypeClass.BandOfMisfits) return false; if (type == Cards.DarkAges2ndEdition.TypeClass.Forager) return false; if (type == Cards.DarkAges2ndEdition.TypeClass.Rats) return false; if (type == Cards.DarkAges2ndEdition.TypeClass.Rebuild) return false; //if (type == Cards.DarkAges2019Errata.TypeClass.BandOfMisfits) // return false; //if (type == Cards.DarkAges2019Errata.TypeClass.Procession) // return false; if (type == Cards.Guilds.TypeClass.Butcher) return false; if (type == Cards.Guilds.TypeClass.Stonemason) return false; if (type == Cards.Guilds2ndEdition.TypeClass.Butcher) return false; if (type == Cards.Guilds2ndEdition.TypeClass.Stonemason) return false; if (type == Cards.Adventures.TypeClass.Duplicate) return false; if (type == Cards.Adventures.TypeClass.Ratcatcher) return false; if (type == Cards.Adventures.TypeClass.Raze) return false; if (type == Cards.Adventures.TypeClass.Transmogrify) return false; if (type == Cards.Adventures2ndEdition.TypeClass.Raze) return false; if (type == Cards.Adventures2ndEdition.TypeClass.Transmogrify) return false; if (type == Cards.Empires.TypeClass.Sacrifice) return false; if (type == Cards.Empires.TypeClass.Overlord) return false; //if (type == Cards.Empires2019Errata.TypeClass.Overlord) // return false; if (type == Cards.Nocturne.TypeClass.Exorcist) return false; if (type == Cards.Renaissance.TypeClass.Cathedral) return false; if (type == Cards.Renaissance.TypeClass.Hideout) return false; if (type == Cards.Renaissance.TypeClass.Priest) return false; if (type == Cards.Renaissance.TypeClass.Recruiter) return false; if (type == Cards.Renaissance.TypeClass.Research) return false; if (type == Cards.Menagerie.TypeClass.Scrap) return false; return true; } protected override IBuyable FindBestCardToBuy(List buyables) { var treasureDensity = ComputeAverageCoinValueInDeck(); Console.WriteLine($"Treasure density: {treasureDensity}"); var scores = ValuateCardsToBuy(buyables); var bestScore = scores.Keys.OrderByDescending(k => k).FirstOrDefault(); if (bestScore >= 0.5d && scores.Any()) return scores[bestScore][_Game.RNG.Next(scores[bestScore].Count)]; return null; } protected virtual Dictionary> ValuateCardsToBuy(IEnumerable buyables) { var scores = new Dictionary>(); var buyablesList = buyables as IList ?? buyables.ToList(); foreach (var buyable in buyablesList) { // We need to compute score based on the original/base cost of the card double score = buyable.TopCard.BaseCost.Coin.Value + 2.5f * buyable.TopCard.BaseCost.Potion.Value + 0.75f * buyable.TopCard.BaseCost.Debt.Value; // I *THINK* this was for testing. I should remove. //if (buyable.TopCard is Cards.Promotional.Sauna || buyable.TopCard is Cards.Universal.Silver) // score += 1.0; if (!ShouldBuy(buyable)) { score = -1d; } else { // Scale based on the original -vs- current cost -- cheaper cards should be more valuable to us! score += Math.Log((score + 1) / ( buyable.CurrentCost.Coin.Value + 0.75f * buyable.Tokens.OfType().Count() + 2.5f * buyable.CurrentCost.Potion.Value + 0.75f * buyable.CurrentCost.Debt.Value + 1 )); } // In general, we don't want to have a ton of terminal Actions in our deck since we can only play so many if (buyable.TopCard.Category.HasFlag(Categories.Action) && buyable.TopCard.Traits.HasFlag(Traits.Terminal)) { // Count the number of non-Ruins terminals (Ruins are almost like curses) and the number of villages var terminalsICanPlay = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Ruins) && c.Traits.HasFlag(Traits.Terminal)); var villagesICanPlay = RealThis.CountAll(RealThis, c => (c.Category.HasFlag(Categories.Action) && c.Traits.HasFlag(Traits.PlusMultipleActions)) || c is Cards.Adventures.CoinOfTheRealm || c is Cards.Adventures2ndEdition.CoinOfTheRealm); var totalDeckSize = RealThis.CountAll(); // If (terminals - villages) / deck_size is > 0.4 (2 out of 5), then we shouldn't buy any more terminals var percentageOfDeadCards = ((double)terminalsICanPlay - villagesICanPlay) / totalDeckSize; if (percentageOfDeadCards > 0.4) score *= Math.Pow(0.2, Math.Pow(percentageOfDeadCards, 2)); } // Scale back the score accordingly if it's near the end of the game and the card is not a Victory card if (GameProgressLeft < 0.25 && !buyable.TopCard.Category.HasFlag(Categories.Victory)) score *= 0.15d; // Scale up Province & Colony scores in late game if (GameProgressLeft < 0.3 && (buyable.TopCard.Type == Cards.Universal.TypeClass.Province || buyable.TopCard.Type == Cards.Prosperity.TypeClass.Colony)) score *= 1.2d; // Never buy non-Province/Colony/Farmland/Tunnel/Castles Victory-only cards early if (GameProgressLeft > 0.71 && buyable.TopCard.Category.HasFlag(Categories.Victory) && !buyable.Category.HasFlag(Categories.Action) && !buyable.Category.HasFlag(Categories.Treasure) && buyable.TopCard.Type != Cards.Universal.TypeClass.Province && buyable.TopCard.Type != Cards.Prosperity.TypeClass.Colony && buyable.TopCard.Type != Cards.Hinterlands.TypeClass.Farmland && buyable.TopCard.Type != Cards.Hinterlands.TypeClass.Tunnel && buyable.TopCard.Type != Cards.Hinterlands2ndEdition.TypeClass.Farmland && buyable.TopCard.Type != Cards.Hinterlands2ndEdition.TypeClass.Tunnel && !buyable.TopCard.Category.HasFlag(Categories.Castle)) score = -1d; // Don't buy these cards until the game progresses this far if ((GameProgressLeft > 0.25 && buyable.TopCard.Type == Cards.Universal.TypeClass.Estate) || (GameProgressLeft > 0.25 && buyable.TopCard.Type == Cards.Alchemy.TypeClass.Vineyard) || (GameProgressLeft > 0.25 && buyable.TopCard.Type == Cards.Alchemy2ndEdition.TypeClass.Vineyard) || (GameProgressLeft > 0.35 && buyable.TopCard.Type == Cards.Base.TypeClass.Gardens) || (GameProgressLeft > 0.35 && buyable.TopCard.Type == Cards.Base2ndEdition.TypeClass.Gardens) || (GameProgressLeft > 0.35 && buyable.TopCard.Type == Cards.Hinterlands.TypeClass.SilkRoad) || (GameProgressLeft > 0.35 && buyable.TopCard.Type == Cards.Hinterlands2ndEdition.TypeClass.SilkRoad) || (GameProgressLeft > 0.35 && buyable.TopCard.Type == Cards.DarkAges.TypeClass.Feodum) || (GameProgressLeft > 0.35 && buyable.TopCard.Type == Cards.DarkAges2ndEdition.TypeClass.Feodum) || (GameProgressLeft > 0.4 && buyable.TopCard.Type == Cards.Universal.TypeClass.Duchy) || (GameProgressLeft > 0.4 && buyable.TopCard.Type == Cards.Intrigue.TypeClass.Duke) || (GameProgressLeft > 0.4 && buyable.TopCard.Type == Cards.Cornucopia.TypeClass.Fairgrounds) || (GameProgressLeft > 0.4 && buyable.TopCard.Type == Cards.Cornucopia2ndEdition.TypeClass.Fairgrounds)) score *= 0.15d; // Distant Lands shouldn't be bought late in the game unless we think we can play them if (GameProgressLeft < 0.25 && buyable.TopCard.Type == Cards.Adventures.TypeClass.DistantLands) score *= 0.45d; // We want to buy these cards more as the game progresses if ((GameProgressLeft < 0.15 && buyable.TopCard.Type == Cards.Universal.TypeClass.Estate) || (GameProgressLeft < 0.15 && buyable.TopCard.Type == Cards.Alchemy.TypeClass.Vineyard) || (GameProgressLeft < 0.15 && buyable.TopCard.Type == Cards.Alchemy2ndEdition.TypeClass.Vineyard) || (GameProgressLeft < 0.20 && buyable.TopCard.Type == Cards.Base.TypeClass.Gardens) || (GameProgressLeft < 0.20 && buyable.TopCard.Type == Cards.Base2ndEdition.TypeClass.Gardens) || (GameProgressLeft < 0.20 && buyable.TopCard.Type == Cards.Hinterlands.TypeClass.SilkRoad) || (GameProgressLeft < 0.20 && buyable.TopCard.Type == Cards.Hinterlands2ndEdition.TypeClass.SilkRoad) || (GameProgressLeft < 0.20 && buyable.TopCard.Type == Cards.DarkAges.TypeClass.Feodum) || (GameProgressLeft < 0.20 && buyable.TopCard.Type == Cards.DarkAges2ndEdition.TypeClass.Feodum) || (GameProgressLeft < 0.25 && buyable.TopCard.Type == Cards.Universal.TypeClass.Duchy) || (GameProgressLeft < 0.25 && buyable.TopCard.Type == Cards.Intrigue.TypeClass.Duke) || (GameProgressLeft < 0.25 && buyable.TopCard.Type == Cards.Cornucopia.TypeClass.Fairgrounds) || (GameProgressLeft < 0.25 && buyable.TopCard.Type == Cards.Cornucopia2ndEdition.TypeClass.Fairgrounds)) score *= 1.1d; // Distant Lands aren't great early, but they're better than Duchy if (GameProgressLeft > 0.75 && (buyable.TopCard.Type == Cards.Adventures.TypeClass.DistantLands)) score *= 0.35d; // Duke/Duchy decision if (buyable.TopCard.Type == Cards.Intrigue.TypeClass.Duke || buyable.TopCard.Type == Cards.Universal.TypeClass.Duchy) { var duchies = RealThis.CountAll(RealThis, c => c is Cards.Universal.Duchy, false); var dukes = RealThis.CountAll(RealThis, c => c is Cards.Intrigue.Duke, false); // If gaining a Duke is not as useful as gaining a Duchy, don't get the Duke if (buyable.TopCard.Type == Cards.Intrigue.TypeClass.Duke && duchies - dukes < 4) score *= 0.95d; // If gaining a Duchy is not as useful as gaining a Duke, don't get the Duchy if (buyable.TopCard.Type == Cards.Universal.TypeClass.Duchy && duchies - dukes >= 4) score *= 0.95d; } // Scale Silk Road score based on how many Victory cards we have if (buyable.TopCard.Type == Cards.Hinterlands.TypeClass.SilkRoad) { var totalVictoryCount = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Victory)); score *= Math.Pow(1.045, 2 * (totalVictoryCount - 6)); } // Scale all Victory cards based on how many Silk Roads we have if (buyable.Category.HasFlag(Categories.Victory)) { var totalSilkRoadCount = RealThis.CountAll(RealThis, c => c is Cards.Hinterlands.SilkRoad || c is Cards.Hinterlands2ndEdition.SilkRoad); score *= Math.Pow(1.045, Math.Max(0, totalSilkRoadCount - 6)); } // Scale Feodum score based on how many Silvers we have if (buyable.TopCard.Type == Cards.DarkAges.TypeClass.Feodum || buyable.TopCard.Type == Cards.DarkAges2ndEdition.TypeClass.Feodum) { var totalSilverCount = RealThis.CountAll(RealThis, c => c is Cards.Universal.Silver); score *= Math.Pow(1.045, 2 * (totalSilverCount - 6)); } if (buyable.TopCard.Type == Cards.Universal.TypeClass.Copper) { // Never buy Copper cards unless we have a Goons or Merchant Guild in play // Or our deck has degenerated and we need actual money in our deck if (!RealThis.InPlay[c => c is Cards.Prosperity.Goons || c is Cards.Prosperity2ndEdition.Goons || c is Cards.Guilds.MerchantGuild || c is Cards.Guilds2ndEdition.MerchantGuild].Any() && RealThis.CountAll(RealThis, c => !(c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && !c.Category.HasFlag(Categories.Night))) > 5 ) score = -1d; } if (buyable.TopCard.Type == Cards.Universal.TypeClass.Silver) { // If Delve is available, always prefer buying Delve to buying Silver // The only exception to this rule could be when Basilica is in play // This functionality isn't accounted for yet, however if (buyablesList.Any(b => b is Cards.Empires.Delve)) score = -1d; //int totalSilverCount = RealThis.CountAll(this, c => c is Cards.Universal.Silver, true, false); var totalFeodumCount = RealThis.CountAll(RealThis, c => c is Cards.DarkAges.Feodum || c is Cards.DarkAges2ndEdition.Feodum, false); if (totalFeodumCount == 0 && (_Game.Table.TableEntities.ContainsKey(Cards.DarkAges.TypeClass.Feodum) || _Game.Table.TableEntities.ContainsKey(Cards.DarkAges2ndEdition.TypeClass.Feodum))) score *= 1.075; score *= Math.Pow(1.04, 2 * totalFeodumCount); if (totalFeodumCount > 0 && GameProgressLeft < 0.25) score /= Math.Max(GameProgressLeft, 0.08); } // Early on, and when we don't have many Silver, give a slight bias to Silvers //if (supply.Type == Cards.Universal.TypeClass.Silver) //{ // score *= Math.Pow(1.04, 2 * Math.Pow(GameProgress, 2)); // int silverCount = RealThis.CountAll(this, c => c is Cards.Universal.Silver, true, false); // int copperCount = RealThis.CountAll(this, c => c is Cards.Universal.Copper, true, false); // int treasureCount = RealThis.CountAll(this, c => c.Category.HasFlag(Category.Treasure) true, false); // int totalCount = RealThis.CountAll(); // //if ( // //score *= //} // Scale scores of terminals & pseudo-terminals based on how many terminals and dead cards we have in our deck // Too many terminals or useless cards means terrible decks if (buyable.TopCard.Traits.HasFlag(Traits.Terminal) || buyable.TopCard.Type == Cards.Seaside.TypeClass.Tactician || buyable.TopCard.Type == Cards.Seaside2ndEdition.TypeClass.Tactician) { var totalCards = RealThis.CountAll(); var deadCards = CountTerminalsAndDeadCardsInDeck(); var ratio = (double)deadCards / totalCards; if (ratio > 0.25) score /= Math.Pow(1.8, 1.25 * Math.Sqrt(ratio)); } if (buyable.TopCard.Type == Cards.Base.TypeClass.Moat || buyable.TopCard.Type == Cards.Base2ndEdition.TypeClass.Moat) { var attackSupplies = RealThis._Game.Table.TableEntities.Values.OfType().Where( s => s.CanGain() && s.TopCard.Category.HasFlag(Categories.Attack)).ToList(); var attackCardsLeft = attackSupplies.Sum(s => s.Count); var attackCardsInTrash = RealThis._Game.Table.Trash.Count(c => attackSupplies.Any(s => s.Type == c.Type)); var attackCardsInDecks = attackSupplies.Sum(s => s.StartingStackSize) - attackCardsLeft - attackCardsInTrash; var attackCardsInMyDeck = CountAll(RealThis, c => c.Category.HasFlag(Categories.Attack)); score *= Math.Pow(1.05, attackCardsInDecks - attackCardsInMyDeck + 0.25 * attackCardsLeft); } if (buyable.TopCard.Type == Cards.Seaside.TypeClass.Lighthouse || buyable.TopCard.Type == Cards.Seaside2ndEdition.TypeClass.Lighthouse) { var attackSupplies = RealThis._Game.Table.TableEntities.Values.OfType().Where( s => s.CanGain() && s.TopCard.Category.HasFlag(Categories.Attack)).ToList(); var attackCardsLeft = attackSupplies.Sum(s => s.Count); var attackCardsInTrash = RealThis._Game.Table.Trash.Count(c => attackSupplies.Any(s => s.Type == c.Type)); var attackCardsInDecks = attackSupplies.Sum(s => s.StartingStackSize) - attackCardsLeft - attackCardsInTrash; var attackCardsInMyDeck = CountAll(RealThis, c => c.Category.HasFlag(Categories.Attack)); 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 (buyable.Tokens.Any(token => token is Cards.Seaside.EmbargoToken) && RealThis._Game.Table.Curse.Any()) { score *= Math.Pow(0.8, Math.Min(buyable.Tokens.Count(token => token is Cards.Seaside.EmbargoToken), RealThis._Game.Table.Curse.Count)); // If Estates are Embargoed (and there are Curses left), make Estates not very nice to buy if (buyable.Type == Cards.Universal.TypeClass.Estate) score = 0.001; } // Peddler -- not really worth 8; scale it back if it's over 5 if (buyable.TopCard.Type == Cards.Prosperity.TypeClass.Peddler) { if (buyable.CurrentCost.Coin > 5) score *= 0.6f; } if (buyable.TopCard.Type == Cards.Prosperity.TypeClass.Mint) { var totalTreasureCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure)); var totalCopperCards = RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper); var cardsToTrash = RealThis.SetAside[Categories.Treasure].Union(RealThis.InPlay[Categories.Treasure]).ToList(); var cardWorth = 0d; foreach (var card in cardsToTrash) { // Since Diadem has no "Cost", we have to create an artificial worth for it if (card is Cards.Cornucopia.Diadem) cardWorth += 7d; // Copper has a slightly negative worth else if (card is Cards.Universal.Copper) cardWorth += -0.25; // Ill-Gotten Gains's worth is significantly less after it's been gained else if (card is Cards.Hinterlands.IllGottenGains || card is Cards.Hinterlands2ndEdition.IllGottenGains) cardWorth -= 1.72; else cardWorth += card.BaseCost.Coin.Value + 2.5 * card.BaseCost.Potion.Value + 1.2 * card.BaseCost.Debt.Value; } score *= Math.Pow(0.975, cardWorth); if (totalTreasureCards - totalCopperCards < cardsToTrash.Count) score *= 0.85; } // Too many Talismans leads to crappy decks -- we should limit ourselves to only a couple // even fewer if there aren't many <= 4-cost non-Victory, non-Talisman cards available if (buyable.TopCard.Type == Cards.Prosperity.TypeClass.Talisman || buyable.TopCard.Type == Cards.Prosperity2ndEdition.TypeClass.Talisman) { score *= talismanLikelihood; score *= Math.Pow(0.95, RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Talisman || c is Cards.Prosperity2ndEdition.Talisman)); Console.WriteLine($"Talisman score: Initial score: {score}"); var pileScore = 0.0; var talismanSupplies = new StringBuilder(); foreach (var talismanableSupply in RealThis._Game.Table.TableEntities.Values.OfType().Where( s => ShouldBuy(s) && s.CanGain() && s.Count >= 2 && !s.TopCard.Category.HasFlag(Categories.Victory) && s.Type != Cards.Prosperity.TypeClass.Talisman && s.Type != Cards.Prosperity2ndEdition.TypeClass.Talisman && s.Type != Cards.Universal.TypeClass.Copper && s.BaseCost <= new Currencies.Coin(4))) { pileScore += 0.1 * Math.Min(10, talismanableSupply.Count); talismanSupplies.AppendFormat("Supply: {0} {1}, ", talismanableSupply.Name, 0.1 * Math.Min(10, talismanableSupply.Count)); } score *= 0.75 * Math.Pow(1.25, Math.Log(pileScore)); Console.WriteLine($"Talisman scores: {talismanSupplies} --- Final score: {score}"); } // 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 (buyable.TopCard.Type == Cards.Alchemy.TypeClass.Potion) { score *= potionLikelihood; score *= Math.Pow(0.8, RealThis.CountAll(RealThis, c => c is Cards.Alchemy.Potion)); score *= Math.Pow(1.1, RealThis._Game.Table.TableEntities.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 // Crown is excluded from this list because it can be used on Treasures if (buyable.TopCard.Type == Cards.Base.TypeClass.ThroneRoom || buyable.TopCard.Type == Cards.Base2ndEdition.TypeClass.ThroneRoom || buyable.TopCard.Type == Cards.Prosperity.TypeClass.KingsCourt || buyable.TopCard.Type == Cards.Prosperity2ndEdition.TypeClass.KingsCourt || buyable.TopCard.Type == Cards.DarkAges.TypeClass.Procession //|| buyable.TopCard.Type == Cards.DarkAges2019Errata.TypeClass.Procession || buyable.TopCard.Type == Cards.Adventures.TypeClass.RoyalCarriage || buyable.TopCard.Type == Cards.Adventures2ndEdition.TypeClass.RoyalCarriage) { var nonTrKcActions = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action) && !(c is Cards.Base.ThroneRoom) && !(c is Cards.Base2ndEdition.ThroneRoom) && !(c is Cards.Prosperity.KingsCourt) && !(c is Cards.Prosperity2ndEdition.KingsCourt) && !(c is Cards.DarkAges.Procession) && !(c is Cards.DarkAges2019Errata.Procession) && !(c is Cards.Adventures.Disciple) && !(c is Cards.Adventures.RoyalCarriage) && !(c is Cards.Adventures2ndEdition.RoyalCarriage)); // Fudge factor nonTrKcActions = (int)(nonTrKcActions * (1.0 + Gaussian.NextGaussian(_Game.RNG) / 8d)); var totalCards = RealThis.CountAll(); // Fudge factor totalCards = (int)(totalCards * (1.0 + Gaussian.NextGaussian(_Game.RNG) / 8d)); var ratio = (double)nonTrKcActions / totalCards; if (ratio < 0.15) score = -1.0; else //score *= Math.Sqrt(ratio / 0.18d); score *= 0.8 * Math.Pow(1.06, 12 * Math.Pow(ratio, 2)); } // Don't overemphasize Golem if we don't have many non-Golem Action cards if (buyable.TopCard.Type == Cards.Alchemy.TypeClass.Golem || buyable.TopCard.Type == Cards.Alchemy2ndEdition.TypeClass.Golem) { var nonGolemActions = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action) && c.Type != Cards.Alchemy.TypeClass.Golem && c.Type != Cards.Alchemy2ndEdition.TypeClass.Golem); // Fudge factor nonGolemActions = (int)(nonGolemActions * (1.0 + Gaussian.NextGaussian(_Game.RNG) / 8d)); var totalCards = RealThis.CountAll(); // Fudge factor totalCards = (int)(totalCards * (1.0 + Gaussian.NextGaussian(_Game.RNG) / 8d)); var ratio = (double)nonGolemActions / totalCards; score *= Math.Sqrt(ratio / 0.18d); } if (buyable.TopCard.Type == Cards.Seaside.TypeClass.Embargo || buyable.TopCard.Type == Cards.Seaside2ndEdition.TypeClass.Embargo //|| buyable.TopCard.Type == Cards.Seaside2019Errata.TypeClass.Embargo ) { var curseGivingSupplies = RealThis._Game.Table.TableEntities.Values.OfType().Where( s => s.Type == Cards.Base.TypeClass.Witch || s.Type == Cards.Base2ndEdition.TypeClass.Witch || s.Type == Cards.Intrigue.TypeClass.Swindler || s.Type == Cards.Intrigue.TypeClass.Torturer || s.Type == Cards.Intrigue2ndEdition.TypeClass.Replace || s.Type == Cards.Intrigue2ndEdition.TypeClass.Swindler || s.Type == Cards.Intrigue2ndEdition.TypeClass.Torturer || s.Type == Cards.Seaside.TypeClass.Ambassador || s.Type == Cards.Seaside.TypeClass.SeaHag || s.Type == Cards.Seaside2ndEdition.TypeClass.Ambassador || s.Type == Cards.Seaside2ndEdition.TypeClass.SeaHag || s.Type == Cards.Alchemy.TypeClass.Familiar || s.Type == Cards.Prosperity.TypeClass.Mountebank || s.Type == Cards.Prosperity2ndEdition.TypeClass.Mountebank || s.Type == Cards.Cornucopia.TypeClass.Jester || s.Type == Cards.Cornucopia.TypeClass.YoungWitch || s.Type == Cards.Cornucopia2ndEdition.TypeClass.Jester || s.Type == Cards.Cornucopia2ndEdition.TypeClass.YoungWitch || s.Type == Cards.Guilds.TypeClass.Soothsayer || s.Type == Cards.Guilds2ndEdition.TypeClass.Soothsayer || s.Type == Cards.Adventures.TypeClass.SwampHag || s.Type == Cards.Adventures2ndEdition.TypeClass.SwampHag || s.Type == Cards.Nocturne.TypeClass.Idol || s.Type == Cards.Renaissance.TypeClass.OldWitch ).ToList(); var curseGivingCardsLeft = curseGivingSupplies.Sum(s => s.Count); var curseGivingCardsInTrash = RealThis._Game.Table.Trash.Count(c => curseGivingSupplies.Any(s => s.Type == c.Type)); var curseGivingCardsInDecks = curseGivingSupplies.Sum(s => s.StartingStackSize) - curseGivingCardsLeft - curseGivingCardsInTrash; var curseGivingCardsInMyDeck = CountAll(RealThis, c => c is Cards.Base.Witch || c is Cards.Intrigue.Swindler || c is Cards.Intrigue.Torturer || c is Cards.Intrigue2ndEdition.Replace || c is Cards.Seaside.Ambassador || c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.Ambassador || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar || c is Cards.Prosperity.Mountebank || c is Cards.Prosperity2ndEdition.Mountebank || c is Cards.Cornucopia.Jester || c is Cards.Cornucopia.YoungWitch || c is Cards.Cornucopia.Followers || c is Cards.Cornucopia2ndEdition.Jester || c is Cards.Cornucopia2ndEdition.YoungWitch || c is Cards.Cornucopia2ndEdition.Followers || c is Cards.Guilds.Soothsayer || c is Cards.Guilds2ndEdition.Soothsayer || c is Cards.Adventures.SwampHag || c is Cards.Adventures2ndEdition.SwampHag || c is Cards.Nocturne.Idol || c is Cards.Renaissance.OldWitch ); // Followers is either in Supply pile or player's piles if (RealThis._Game.Table.TableEntities.ContainsKey(Cards.Cornucopia.TypeClass.PrizeSupply) || RealThis._Game.Table.TableEntities.ContainsKey(Cards.Cornucopia2ndEdition.TypeClass.PrizeSupply)) { var followersInSupply = 0; if (RealThis._Game.Table.TableEntities.ContainsKey(Cards.Cornucopia.TypeClass.PrizeSupply)) followersInSupply += ((ISupply)RealThis._Game.Table.TableEntities[Cards.Cornucopia.TypeClass.PrizeSupply]).Count(c => c is Cards.Cornucopia.Followers); if (RealThis._Game.Table.TableEntities.ContainsKey(Cards.Cornucopia2ndEdition.TypeClass.PrizeSupply)) followersInSupply += ((ISupply)RealThis._Game.Table.TableEntities[Cards.Cornucopia2ndEdition.TypeClass.PrizeSupply]).Count(c => c is Cards.Cornucopia2ndEdition.Followers); var followersInTrash = RealThis._Game.Table.Trash.Count(c => c is Cards.Cornucopia.Followers || c is Cards.Cornucopia2ndEdition.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)(RealThis._Game.Table.TableEntities[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 (buyable.TopCard.Type == Cards.Base.TypeClass.Witch || buyable.TopCard.Type == Cards.Base2ndEdition.TypeClass.Witch) { score *= Math.Pow(0.8, (9.8d - (1d + Gaussian.NextGaussian(_Game.RNG) / 12d) * Math.Sqrt(10) * Math.Sqrt((double)RealThis._Game.Table.Curse.Count / (RealThis._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 (buyable.TopCard.Type == Cards.Seaside.TypeClass.SeaHag || buyable.TopCard.Type == Cards.Seaside2ndEdition.TypeClass.SeaHag) { score *= Math.Pow(0.8, 9.8d - (1d + Gaussian.NextGaussian(_Game.RNG) / 12d) * Math.Sqrt(10) * Math.Sqrt((double)RealThis._Game.Table.Curse.Count / (RealThis._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 (buyable.TopCard.Type == Cards.Alchemy.TypeClass.Familiar) { score *= Math.Pow(0.8, (9.8d - (1d + Gaussian.NextGaussian(_Game.RNG) / 12d) * Math.Sqrt(10) * Math.Sqrt((double)RealThis._Game.Table.Curse.Count / (RealThis._Game.Players.Count - 1))) * 5.5335 / 10); } // Giant gets slightly less valuable with fewer & fewer curses, down to about 3.6 when there are none // We shouldn't actually make it any more likely to get Giant when there are lots of curses if (buyable.TopCard.Type == Cards.Adventures.TypeClass.Giant || buyable.TopCard.Type == Cards.Adventures2ndEdition.TypeClass.Giant) { score *= Math.Pow(0.8, (0.87d - Math.Sqrt((1d + (2 * Gaussian.NextGaussian(_Game.RNG) - 1.0) / 12d) * Math.Pow((double)RealThis._Game.Table.Curse.Count / (RealThis._Game.Players.Count - 1) / 10, 0.25))) * 1.7); } // Swamp Hag 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 Swamp Hag when there are lots of curses if (buyable.TopCard.Type == Cards.Adventures.TypeClass.SwampHag || buyable.TopCard.Type == Cards.Adventures2ndEdition.TypeClass.SwampHag) { score *= Math.Pow(0.8, (9.8d - (1d + Gaussian.NextGaussian(_Game.RNG) / 12d) * Math.Sqrt(10) * Math.Sqrt((double)RealThis._Game.Table.Curse.Count / (RealThis._Game.Players.Count - 1))) * 5.5335 / 10); } // Limit the number of Contrabands we'll buy to a fairly small amount (1 per every 25 cards or so) if (buyable.TopCard.Type == Cards.Prosperity.TypeClass.Contraband) { var contrabandsICanPlay = RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Contraband); var totalDeckSize = RealThis.CountAll(); var percentageOfContrabands = (double)contrabandsICanPlay / totalDeckSize; if (percentageOfContrabands > 0.04) 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 20 cards or so) if (buyable.TopCard.Type == Cards.Seaside.TypeClass.Outpost || buyable.TopCard.Type == Cards.Seaside2ndEdition.TypeClass.Outpost) { var outpostsICanPlay = RealThis.CountAll(RealThis, c => c is Cards.Seaside.Outpost || c is Cards.Seaside2ndEdition.Outpost); var totalDeckSize = RealThis.CountAll(); var percentageOfOutposts = (double)outpostsICanPlay / totalDeckSize; if (percentageOfOutposts > 0.05) score *= Math.Pow(0.2, Math.Pow(percentageOfOutposts, 2)); } // Discourage Embassy buys in turns 1-4 (decaying) and when opponents' decks are almost depleted (< 5 cards) if (buyable.TopCard.Type == Cards.Hinterlands.TypeClass.Embassy) { score *= 0.15 * Math.Min(5f, RealThis._Game.TurnsTaken.Count(t => t.Player == RealThis)) + 0.25; var opponents = RealThis._Game.Players.Where(p => p != RealThis).ToList(); var opponentsWithSmallDecks = opponents.Select(p => p.DrawPile.LookThrough(c => true).Count).Count(c => c < 5); score *= 1 - (float)opponentsWithSmallDecks / opponents.Count / 4; } // Only buy Spice Merchant if it's value in our deck would be not terrible if (buyable.TopCard.Type == Cards.Hinterlands.TypeClass.SpiceMerchant || buyable.TopCard.Type == Cards.Hinterlands2ndEdition.TypeClass.SpiceMerchant) { var value = ComputeValueInDeck(buyable.TopCard); score *= value / buyable.TopCard.BaseCost.Coin.Value; } // 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 (buyable.TopCard.Type == Cards.Hinterlands.TypeClass.Farmland || buyable.TopCard.Type == Cards.Hinterlands2ndEdition.TypeClass.Farmland) { if (RealThis.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 (RealThis.Hand[Categories.Curse].Any()) 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 (RealThis.Hand.All(c => !_Game.Table.TableEntities.Values.OfType().Any(s => s.CanGain() && s.CurrentCost == _Game.ComputeCost(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 (RealThis.Hand.Any(c => c.Category.HasFlag(Categories.Victory) && _Game.Table.TableEntities.Values.OfType().Any( s => s.CanGain() && s.TopCard.Category.HasFlag(Categories.Victory) && s.VictoryPoints > c.VictoryPoints && s.CurrentCost == _Game.ComputeCost(c) + new Currencies.Coin(2)))) { score *= 1.1; } // I dunno after that... will require testing. In general, be cautious else { score *= 0.75; } } } if (buyable.TopCard.Type == Cards.Hinterlands.TypeClass.NobleBrigand || buyable.TopCard.Type == Cards.Hinterlands2ndEdition.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 (RealThis._Game.TurnsTaken.Count(t => t.Player == RealThis) < 3) score = -1.0; double nbTotalSilverGold = RealThis._Game.Players.Where(p => p != RealThis).Sum(p => p.CountAll(RealThis, c => c is Cards.Universal.Silver || c is Cards.Universal.Gold)) / (RealThis._Game.Players.Count - 1); double nbTotalCards = RealThis._Game.Players.Where(p => p != RealThis).Sum(p => p.CountAll()) / (RealThis._Game.Players.Count - 1); if (nbTotalSilverGold < 2 || nbTotalSilverGold / nbTotalCards < 0.1) score *= 0.5; } if (buyable.TopCard.Type == Cards.Guilds.TypeClass.Doctor || buyable.TopCard.Type == Cards.Guilds2ndEdition.TypeClass.Doctor) { if (RealThis.Currency.Coin.Value == buyable.CurrentCost.Coin.Value) score *= 0.75; Console.WriteLine("Doctor new score: {0}", score); } if (buyable.TopCard.Type == Cards.Guilds.TypeClass.Herald || buyable.TopCard.Type == Cards.Guilds2ndEdition.TypeClass.Herald) { if (RealThis.Currency.Coin.Value == buyable.CurrentCost.Coin.Value) score *= 0.75; Console.WriteLine("Herald new score: {0}", score); } if (buyable.TopCard.Type == Cards.Guilds.TypeClass.Masterpiece || buyable.TopCard.Type == Cards.Guilds2ndEdition.TypeClass.Masterpiece) { // We'd have to gain at least 2 Silvers for this to be worth-while if (RealThis.Currency.Coin.Value <= (buyable.CurrentCost.Coin + new Currencies.Coin(1)).Value) score *= 0.75; else { // Victory cards that make gaining lots of Silvers good var totalGardensCount = RealThis.CountAll(RealThis, c => c is Cards.Base.Gardens); var availableGardens = 0; if (RealThis._Game.Table.TableEntities.ContainsKey(Cards.Base.TypeClass.Gardens)) availableGardens = ((ISupply)RealThis._Game.Table[Cards.Base.TypeClass.Gardens]).Count; if (RealThis._Game.Table.TableEntities.ContainsKey(Cards.Base2ndEdition.TypeClass.Gardens)) availableGardens = ((ISupply)RealThis._Game.Table[Cards.Base2ndEdition.TypeClass.Gardens]).Count; var totalFeodumCount = RealThis.CountAll(RealThis, c => c is Cards.DarkAges.Feodum || c is Cards.DarkAges2ndEdition.Feodum); var availableFeodums = 0; if (RealThis._Game.Table.TableEntities.ContainsKey(Cards.DarkAges.TypeClass.Feodum)) availableFeodums = ((ISupply)RealThis._Game.Table[Cards.DarkAges.TypeClass.Feodum]).Count; if (RealThis._Game.Table.TableEntities.ContainsKey(Cards.DarkAges2ndEdition.TypeClass.Feodum)) availableFeodums = ((ISupply)RealThis._Game.Table[Cards.DarkAges2ndEdition.TypeClass.Feodum]).Count; // ALL THE FACTORS score *= Math.Pow(1.04 + 0.005 * totalGardensCount + 0.025 * totalFeodumCount + 0.0025 * availableGardens + 0.005 * availableFeodums, RealThis.Currency.Coin.Value - buyable.CurrentCost.Coin.Value); } // Catapulting a Masterpiece works quite well if (RealThis.CountAll(RealThis, c => c is Cards.Empires.Catapult) > 0) score *= 1.1; Console.WriteLine("Masterpiece new score: {0}", score); } if (buyable.TopCard.Type == Cards.Guilds.TypeClass.Stonemason || buyable.TopCard.Type == Cards.Guilds2ndEdition.TypeClass.Stonemason) { if (RealThis.Currency.Coin.Value == buyable.CurrentCost.Coin.Value) score *= 0.5; Console.WriteLine("Stonemason new score: {0}", score); } // Too many Bridge Trolls is diminishing returns on the Attack, so let's limit them to about 1 for every 15 cards if (buyable.TopCard.Type == Cards.Adventures.TypeClass.BridgeTroll || buyable.TopCard.Type == Cards.Adventures2ndEdition.TypeClass.BridgeTroll) { var bridgeTrollsICanPlay = RealThis.CountAll(RealThis, c => c is Cards.Adventures.BridgeTroll || c is Cards.Adventures2ndEdition.BridgeTroll); var totalDeckSize = RealThis.CountAll(); var percentageOfBridgeTrolls = (double)bridgeTrollsICanPlay / totalDeckSize; if (percentageOfBridgeTrolls > 0.0666) score *= Math.Pow(0.2, Math.Pow(percentageOfBridgeTrolls, 2)); } // Gear is favored a bit too heavily in the early game normally if (buyable.TopCard.Type == Cards.Adventures.TypeClass.Gear || buyable.TopCard.Type == Cards.Adventures2ndEdition.TypeClass.Gear) { if (GameProgressLeft > 0.85) score *= 0.95; } // Too many Haunted Woods is diminishing returns on the Attack, so let's limit them to about 1 for every 15 cards if (buyable.TopCard.Type == Cards.Adventures.TypeClass.HauntedWoods || buyable.TopCard.Type == Cards.Adventures2ndEdition.TypeClass.HauntedWoods) { var hauntedWoodsICanPlay = RealThis.CountAll(RealThis, c => c is Cards.Adventures.HauntedWoods || c is Cards.Adventures2ndEdition.HauntedWoods); var totalDeckSize = RealThis.CountAll(); var percentageOfHauntedWoods = (double)hauntedWoodsICanPlay / totalDeckSize; if (percentageOfHauntedWoods > 0.0667) score *= Math.Pow(0.2, Math.Pow(percentageOfHauntedWoods, 2)); } // Don't buy Magpie if we don't have much Treasure in our deck // Also, don't buy Magpie if we've already got 2 or more if (buyable.TopCard.Type == Cards.Adventures.TypeClass.Magpie) { var treasuresICanPlay = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure)); var magpiesICanPlay = RealThis.CountAll(RealThis, c => c is Cards.Adventures.Magpie); var totalDeckSize = RealThis.CountAll(); var percentageOfTreasures = (double)treasuresICanPlay / totalDeckSize; if (percentageOfTreasures < 0.71) score *= Math.Pow(0.2, Math.Pow(percentageOfTreasures, 2)); if (magpiesICanPlay >= 1) score *= percentageOfTreasures; if (magpiesICanPlay >= 2) score *= 0.1; } // Too many Relics is diminishing returns on the Attack, so let's limit them to about 1 for every 10 cards if (buyable.TopCard.Type == Cards.Adventures.TypeClass.Relic || buyable.TopCard.Type == Cards.Adventures2ndEdition.TypeClass.Relic) { var relicsICanPlay = RealThis.CountAll(RealThis, c => c is Cards.Adventures.Relic || c is Cards.Adventures2ndEdition.Relic); var totalDeckSize = RealThis.CountAll(); var percentageOfRelics = (double)relicsICanPlay / totalDeckSize; if (percentageOfRelics > 0.1) score *= Math.Pow(0.2, Math.Pow(percentageOfRelics, 2)); } // We can't play Reserve cards well currently, so let's not favor them too heavily if (buyable.Category.HasFlag(Categories.Reserve)) { score *= 0.90; } if (buyable.TopCard.Type == Cards.Adventures.TypeClass.Page || buyable.TopCard.Type == Cards.Adventures.TypeClass.Peasant) { score += GameProgressLeft; } if (buyable.TopCard.Category.HasFlag(Categories.Castle)) { score *= castlesLikelihood; } if (buyable.TopCard.Type == Cards.Empires.TypeClass.BustlingVillage) { score *= Math.Pow(1.075, RealThis.CountAll(RealThis, c => c is Cards.Empires.Settlers)); } if (buyable.TopCard.Type == Cards.Empires.TypeClass.CityQuarter) { // City Quarter likes lots of Action cards and prefers a decent amount of terminals score *= 0.25 * Math.Pow(1.7, 5 * RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action)) / (double)RealThis.CountAll()); if (RealThis.Currency.Coin.Value < 3) score = -1; } if (buyable.TopCard.Type == Cards.Empires.TypeClass.Emporium) { if (RealThis.InPlayAndSetAside[Categories.Action].Count < 5) score *= 0.8; else score *= 1.6; } if (buyable.TopCard.Type == Cards.Empires.TypeClass.Encampment) { score *= 0.8 * Math.Pow(1.15, RealThis.CountAll(RealThis, c => c is Cards.Universal.Gold || c is Cards.Empires.Plunder)); } if (buyable.TopCard.Type == Cards.Empires.TypeClass.Engineer) { // Scale based on our expected value from each Supply pile double factor = 0; foreach (var supp in _Game.Table.TableEntities.Values.OfType().Where(s => s.CanGain() && ShouldBuy(s) && s.BaseCost <= new Currency(4))) { int count; if (supp.Type == Cards.Empires.TypeClass.EncampmentPlunder) count = supp[Cards.Empires.TypeClass.Encampment].Count; else if (supp.Type == Cards.Empires.TypeClass.GladiatorFortune) count = supp[Cards.Empires.TypeClass.Gladiator].Count; else if (supp.Type == Cards.Empires.TypeClass.PatricianEmporium) count = supp[Cards.Empires.TypeClass.Patrician].Count; else if (supp.Type == Cards.Empires.TypeClass.SettlersBustlingVillage) count = supp[Cards.Empires.TypeClass.Settlers].Count; else if (supp.Type == Cards.Empires.TypeClass.Castles) count = supp[c => c is Cards.Empires.HumbleCastle || c is Cards.Empires.CrumblingCastle].Count; else count = supp.Count; factor += count / 10d * Math.Pow(supp.BaseCost.Coin.Value / 4f, 2) / 2; } score *= 0.75 * Math.Pow(1.12, factor); Console.WriteLine($"Engineer factor: {factor}, {score}"); // Scale back based on how many Engineers we have score *= Math.Pow(0.8, RealThis.CountAll(RealThis, c => c is Cards.Empires.Engineer)); Console.WriteLine($"Engineer count: {RealThis.CountAll(RealThis, c => c is Cards.Empires.Engineer)}, {score}"); // If it's bad, just don't get it if (score < 1) score = -1d; } if (buyable.Type == Cards.Empires.TypeClass.RoyalBlacksmith) { // Royal Blacksmith is bad early or if we've got a lot of Coppers var allCards = RealThis.CountAll(); var coppers = RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper); var nonCopperTreasures = RealThis.CountAll(RealThis, c => !(c is Cards.Universal.Copper) && c.Category.HasFlag(Categories.Treasure)); score *= nonCopperTreasures / (double)(allCards + coppers); if (RealThis.Currency.Coin.Value < 3) score = -1; } if (buyable.TopCard.Type == Cards.Empires.TypeClass.Rocks) { score *= Math.Pow(1.075, RealThis.CountAll(RealThis, c => c is Cards.Empires.Catapult)); } if (buyable.TopCard.Type == Cards.Empires.TypeClass.Temple) { score *= 0.85 * Math.Pow(1.1, buyable.Tokens.Count(t => t is Cards.Prosperity.VictoryToken)); } if (buyable.TopCard.Category.HasFlag(Categories.Victory)) { score *= Math.Pow(1.1, InPlayAndSetAside[Cards.Empires.TypeClass.Groundskeeper].Count); } if (buyable.TopCard.Type == Cards.Empires.TypeClass.Villa) { var extraActions = Hand[c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c)].Count; if (extraActions > 0) score *= Math.Pow(1.1, Math.Min(2, extraActions)); else score *= 0.7; } // Don't buy too many Night cards other than Raider if (buyable.Category.HasFlag(Categories.Night) && buyable.Type != Cards.Nocturne.TypeClass.Raider) { var countInDeck = RealThis.CountAll(RealThis, c => c.Type == buyable.Type); var ratio = (double)countInDeck / RealThis.CountAll(RealThis); if (ratio > 0.2) score *= 0.10; } // If we have a decent percentage of Copper/trashable Treasures, Pooka is reliably +4 cards if (buyable.Type == Cards.Nocturne.TypeClass.Pooka) { var trashableTreasures = RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper || c is Cards.Prosperity.Loan || c is Cards.Prosperity.Quarry || c is Cards.Prosperity2ndEdition.Loan || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece || c is Cards.Adventures.CoinOfTheRealm || c is Cards.Adventures2ndEdition.CoinOfTheRealm || c is Cards.Empires.Rocks || c is Cards.Nocturne.HauntedMirror || c is Cards.Nocturne.MagicLamp || c is Cards.Nocturne.Goat || c is Cards.Nocturne.LuckyCoin ); var ratio = (double)trashableTreasures / RealThis.CountAll(RealThis); score *= Math.Pow(1.05 * ratio / 0.5, 3.75); } // Special case for Shepherd -- more victory cards = more drawing if (buyable.Type == Cards.Nocturne.TypeClass.Shepherd) { var allVictoryOnlyCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && !c.Category.HasFlag(Categories.Night)); var allTreasureCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure)); if ((double)allVictoryOnlyCards / allTreasureCards > 0.5d) score *= 2.0d; } // The cost of Events is so wildly different from their actual valuation // (e.g. Delve @ 2 coins is way, WAY better than buying a Silver directly) // We'll have to manually adjust the score for the various Events // Also, lots of logic determining what to play needs to be dependent on what Events are available // (e.g. with Alms in play, it's often better to NOT play Treasures) // Most Events are really bad a lot of the time and should be set to -1 in those cases if (buyable.TopCard is Cards.Adventures.Alms && (InPlayAndSetAside[Categories.Treasure].Any() || RealThis.CurrentTurn.CardsBought.Contains(buyable))) { score = -1d; } // Ball is pretty sweet if we've already got our -1 Coin token if (buyable.TopCard is Cards.Adventures.Ball && TokenPiles[Cards.Adventures.TypeClass.MinusOneCoinToken].Count > 0) { score *= 1.5; } if (buyable.TopCard is Cards.Adventures.Bonfire) { var cardsToTrash = FindBestCardsToTrash(InPlayAndSetAside[c => !(c is Cards.DarkAges.Fortress)], 2, true); if (!cardsToTrash.Any()) score = -1d; } if (buyable.TopCard is Cards.Adventures.Borrow || buyable.TopCard is Cards.Adventures2ndEdition.Borrow) { if (RealThis.CurrentTurn.CardsBought.Contains(buyable)) score = -1d; else { var borrowBuyables = RealThis._Game.Table.TableEntities.Values.OfType().Where(s => !(s is Cards.Adventures.Borrow) && !(s is Cards.Adventures2ndEdition.Borrow) && s.CanBuy(RealThis, RealThis.Currency) && ShouldBuy(s) ); // Yay recursion! var borrowScores = ValuateCardsToBuy(borrowBuyables); var bestScore = borrowScores.Keys.OrderByDescending(k => k).FirstOrDefault(); var previousBestScore = bestScore; // Now see what our best buy is if we borrow a coin borrowBuyables = RealThis._Game.Table.TableEntities.Values.OfType().Where(s => !(s is Cards.Adventures.Borrow) && !(s is Cards.Adventures2ndEdition.Borrow) && s.CanBuy(RealThis, RealThis.Currency + new Currencies.Coin(1)) && ShouldBuy(s) ); borrowScores = ValuateCardsToBuy(borrowBuyables); bestScore = borrowScores.Keys.OrderByDescending(k => k).FirstOrDefault(); // All or nothing with Borrow! if (bestScore > 3 && bestScore > previousBestScore + 1.2) score = 99d; else score = -1; } } if (buyable.TopCard is Cards.Adventures.Expedition) { var averageCardWorth = RealThis.SumAll(RealThis, c => true, c => c.BaseCost.Coin.Value + 2.5 * c.BaseCost.Potion.Value + 0.8 * c.BaseCost.Debt.Value) / RealThis.CountAll(); score *= 0.85 * Math.Pow(1.07, averageCardWorth); } #region Token Events if (buyable.TopCard is Cards.Adventures.LostArts || buyable.TopCard is Cards.Adventures2ndEdition.LostArts) { var existingLostArts = _Game.Table.TableEntities.FirstOrDefault( skv => skv.Value.Tokens.Any( t => t is Cards.Adventures.PlusOneActionToken && ((Cards.Adventures.PlusOneActionToken)t).Owner == RealThis ) ).Value; var supply = _Game.Table.TableEntities.ContainsKey(Cards.Adventures.TypeClass.LostArts) ? _Game.Table[Cards.Adventures.TypeClass.LostArts] : _Game.Table[Cards.Adventures2ndEdition.TypeClass.LostArts]; var bestOptionResult = DecideLostArts(new Choice("", supply, new SupplyCollection(_Game.Table.TableEntities.FindAll(s => s.Category.HasFlag(Categories.Action))), ChoiceOutcome.Select, RealThis, false)); if (existingLostArts == bestOptionResult.Supply) score = -1.0; var cardsOfTheType = RealThis.CountAll(RealThis, c => bestOptionResult.Supply.Types.Contains(c.Type)); score *= 0.9 * Math.Pow(1.04, cardsOfTheType); } if (buyable.TopCard is Cards.Adventures.Pathfinding || buyable.TopCard is Cards.Adventures2ndEdition.Pathfinding) { var existingLostArts = _Game.Table.TableEntities.FirstOrDefault( skv => skv.Value.Tokens.Any( t => t is Cards.Adventures.PlusOneCardToken && ((Cards.Adventures.PlusOneCardToken)t).Owner == RealThis ) ).Value; var supply = _Game.Table.TableEntities.ContainsKey(Cards.Adventures.TypeClass.Pathfinding) ? _Game.Table[Cards.Adventures.TypeClass.Pathfinding] : _Game.Table[Cards.Adventures2ndEdition.TypeClass.Pathfinding]; var bestOptionResult = DecidePathfinding(new Choice("", supply, new SupplyCollection(_Game.Table.TableEntities.FindAll(s => s.Category.HasFlag(Categories.Action))), ChoiceOutcome.Select, RealThis, false)); if (existingLostArts == bestOptionResult.Supply) score = -1.0; var cardsOfTheType = RealThis.CountAll(RealThis, c => bestOptionResult.Supply.Types.Contains(c.Type)); score *= 0.9 * Math.Pow(1.04, cardsOfTheType); } if (buyable.TopCard is Cards.Adventures.Seaway || buyable.TopCard is Cards.Adventures2ndEdition.Seaway) { var existingLostArts = _Game.Table.TableEntities.FirstOrDefault( skv => skv.Value.Tokens.Any( t => t is Cards.Adventures.PlusOneBuyToken && ((Cards.Adventures.PlusOneBuyToken)t).Owner == RealThis ) ).Value; var supply = _Game.Table.TableEntities.ContainsKey(Cards.Adventures.TypeClass.Seaway) ? _Game.Table[Cards.Adventures.TypeClass.Seaway] : _Game.Table[Cards.Adventures2ndEdition.TypeClass.Seaway]; var bestOptionResult = DecideSeaway(new Choice("", supply, new SupplyCollection(_Game.Table.TableEntities.FindAll(s => s.Category.HasFlag(Categories.Action))), ChoiceOutcome.Select, RealThis, false)); if (existingLostArts == bestOptionResult.Supply) score = -1.0; var cardsOfTheType = RealThis.CountAll(RealThis, c => bestOptionResult.Supply.Types.Contains(c.Type)); score *= 0.9 * Math.Pow(1.04, cardsOfTheType); } if (buyable.TopCard is Cards.Adventures.Training || buyable.TopCard is Cards.Adventures2ndEdition.Training) { var existingLostArts = _Game.Table.TableEntities.FirstOrDefault( skv => skv.Value.Tokens.Any( t => t is Cards.Adventures.PlusOneCoinToken && ((Cards.Adventures.PlusOneCoinToken)t).Owner == RealThis ) ).Value; var supply = _Game.Table.TableEntities.ContainsKey(Cards.Adventures.TypeClass.Training) ? _Game.Table[Cards.Adventures.TypeClass.Training] : _Game.Table[Cards.Adventures2ndEdition.TypeClass.Training]; var bestOptionResult = DecideTraining(new Choice("", supply, new SupplyCollection(_Game.Table.TableEntities.FindAll(s => s.Category.HasFlag(Categories.Action))), ChoiceOutcome.Select, RealThis, false)); if (existingLostArts == bestOptionResult.Supply) score = -1.0; var cardsOfTheType = RealThis.CountAll(RealThis, c => bestOptionResult.Supply.Types.Contains(c.Type)); score *= 0.9 * Math.Pow(1.04, cardsOfTheType); } #endregion if (buyable.TopCard is Cards.Adventures2ndEdition.Mission || buyable.TopCard is Cards.Adventures.Mission) { if (RealThis.CurrentTurn.CardsBought.Contains(buyable) || RealThis.CurrentTurn.GrantedBy != null) score = -1d; else score *= 0.874; // What should this scaling factor be? I don't know... } if (buyable.TopCard is Cards.Adventures.Pilgrimage) { if (RealThis.CurrentTurn.CardsBought.Contains(buyable)) score = -1d; else score *= 0.64; } if (buyable.TopCard is Cards.Adventures.Quest) { if (Hand[Categories.Attack].Any() || Hand[Cards.Universal.TypeClass.Curse].Count >= 2 || Hand.Count >= 6) score = 5.8d; else score = -1d; } if (buyable.TopCard is Cards.Adventures.Raid || buyable.TopCard is Cards.Adventures2ndEdition.Raid) { score *= 0.8 * Math.Pow(1.15, InPlayAndSetAside[Cards.Universal.TypeClass.Silver].Count); } if ((buyable.TopCard is Cards.Adventures.Save || buyable.TopCard is Cards.Adventures2ndEdition.Save) && RealThis.CurrentTurn.CardsBought.Contains(buyable)) { score = -1d; } if (buyable.TopCard is Cards.Adventures.Trade) { var trashables = Hand[ c => c is Cards.Universal.Copper || c is Cards.Universal.Curse || c is Cards.DarkAges.Hovel || c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece || c is Cards.Empires.Rocks || c.Category.HasFlag(Categories.Ruins) || !ShouldBuy(c.Type)].Count; if (trashables == 0) score = -1d; else if (trashables == 1) score *= 0.75; else if (trashables == 2) score *= 1.2; } if (buyable.TopCard is Cards.Empires.Advance) { var actions = Hand[Categories.Action].OrderBy(c => c.BaseCost); if (!actions.Any()) score = -1d; else { var cheapestAction = actions.First(); score = 6 - (cheapestAction.BaseCost.Coin.Value + 2.5 * cheapestAction.BaseCost.Potion.Value + 0.8 * cheapestAction.BaseCost.Debt.Value); } } if (buyable.TopCard is Cards.Empires.Annex) { // Base score for Annex should be a bit more than the score for Duchy score = 5.5; // Subtract 5 because we'll choose the "worst 5" to keep there var crapDiscardCount = Math.Max(0, DiscardPile.LookThrough( c => c is Cards.Universal.Copper || c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins) || c.Category.HasFlag(Categories.Shelter) || c.Category.HasFlag(Categories.Victory) && !(c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure) || c.Category.HasFlag(Categories.Night))).Count - 5); var goodDiscardCards = DiscardPile.LookThrough( c => !(c is Cards.Universal.Copper) && !(c is Cards.Universal.Curse) && !c.Category.HasFlag(Categories.Ruins) && !c.Category.HasFlag(Categories.Shelter) && !(c.Category.HasFlag(Categories.Victory) && !(c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure) || c.Category.HasFlag(Categories.Night)))); var discardValue = (goodDiscardCards.Sum(c => ComputeValueInDeck(c)) - crapDiscardCount) / (goodDiscardCards.Count + crapDiscardCount); var crapDeckCount = DrawPile.LookThrough( c => c is Cards.Universal.Copper || c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins) || c.Category.HasFlag(Categories.Shelter) || c.Category.HasFlag(Categories.Victory) && !(c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure) || c.Category.HasFlag(Categories.Night))).Count; var goodDeckCards = DrawPile.LookThrough( c => !(c is Cards.Universal.Copper) && !(c is Cards.Universal.Curse) && !c.Category.HasFlag(Categories.Ruins) && !c.Category.HasFlag(Categories.Shelter) && !(c.Category.HasFlag(Categories.Victory) && !(c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure) || c.Category.HasFlag(Categories.Night)))); var deckValue = (goodDeckCards.Sum(c => ComputeValueInDeck(c)) - crapDeckCount) / (goodDeckCards.Count + crapDeckCount); var cardValueRatio = discardValue > 0 ? deckValue >= 1 ? discardValue / deckValue : discardValue : 0; var cardCountDiscard = goodDiscardCards.Count + crapDiscardCount; var cardCountDeck = goodDeckCards.Count + crapDeckCount; var cardCountRatio = cardCountDiscard > 0 ? cardCountDeck == 0 ? Math.Sqrt(cardCountDiscard) : cardCountDiscard / (double)cardCountDeck : 0; var goodCardRatioDiscard = goodDiscardCards.Count / (double)(goodDiscardCards.Count + crapDiscardCount); var goodCardRatioDeck = goodDeckCards.Count / (double)(goodDeckCards.Count + crapDeckCount); var goodCardRatioRatio = goodCardRatioDiscard > 0 ? goodCardRatioDeck > 0 ? (goodDiscardCards.Count + goodDeckCards.Count) / (double)(goodDiscardCards.Count + crapDiscardCount + goodDeckCards.Count + crapDeckCount) / goodCardRatioDeck : 1.5 * goodCardRatioDiscard : 0; // Average the 3 scores to get a final score (needs tweaking) var finalScale = (Math.Pow(cardValueRatio / 2.5, 2) + cardCountRatio / 3 + goodCardRatioRatio) / 3; if (discardValue < 0.45 * deckValue || goodDeckCards.Count == 0 || crapDeckCount == 0 || finalScale < 0.2 ) score = -1d; else { score *= finalScale; if (!_Game.Table.Duchy.CanGain()) score *= 0.01; // Scale back the value based on how many Debt tokens we'd be gaining if (RealThis.Currency.Coin < 5) score /= Math.Pow(5 - RealThis.Currency.Coin.Value, 2); } } if (buyable.TopCard is Cards.Empires.Banquet) { var gainableViaBanquet = _Game.Table.TableEntities.Values.OfType() .Where( b => b.CanBuy(RealThis) && !b.TopCard.Category.HasFlag(Categories.Victory) && ShouldBuy(b) && b.CurrentCost >= new Currency(4) && b.CurrentCost <= new Currency(5)); var banquetScores = ValuateCardsToBuy(gainableViaBanquet); if (banquetScores.Select(k => k.Key).OrderByDescending(k => k).FirstOrDefault() > 4.2) score = 3.5d; else score = -1d; } if (buyable.TopCard is Cards.Empires.Conquest) { score *= 0.9 * Math.Pow(1.1, 2 + RealThis.CurrentTurn.CardsGained.Count(c => c is Cards.Universal.Silver)); } if (buyable.TopCard is Cards.Empires.Delve) { score *= 1.5d; var totalFeodumCount = RealThis.CountAll(RealThis, c => c is Cards.DarkAges.Feodum || c is Cards.DarkAges2ndEdition.Feodum, false); if (totalFeodumCount == 0 && (_Game.Table.TableEntities.ContainsKey(Cards.DarkAges.TypeClass.Feodum) || _Game.Table.TableEntities.ContainsKey(Cards.DarkAges2ndEdition.TypeClass.Feodum))) score *= 1.075; score *= Math.Pow(1.04, 2 * totalFeodumCount); if (totalFeodumCount > 0 && GameProgressLeft < 0.25) score /= Math.Max(GameProgressLeft, 0.08); } if (buyable.TopCard is Cards.Empires.Dominate && !_Game.Table.Province.CanGain()) { score = -1d; } if (buyable.TopCard is Cards.Empires.Donate) { // Never buy Donate the first 3 turns if (_Game.TurnsTaken.Count / _Game.Players.Count <= 3) score = -1d; else { bool trashPredicate(IPoints c) => c is Cards.Universal.Curse || c is Cards.DarkAges.Hovel || c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate || c.Category.HasFlag(Categories.Ruins) || (!_Game.Table.Curse.CanGain() && (c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar )) || (GameProgressLeft > 0.8 && c is Cards.Universal.Estate) || (GameProgressLeft < 0.5 && c is Cards.Universal.Copper); // Curses count triple for Donate scoring var cursesToTrash = RealThis.CountAll(RealThis, c => c is Cards.Universal.Curse); var cardsToTrash = RealThis.CountAll(RealThis, trashPredicate) + 2 * cursesToTrash; var cardsInDeck = RealThis.CountAll(); var coinValue = ComputeAverageCoinValueInDeck(c => c.Category.HasFlag(Categories.Treasure) && !trashPredicate(c)); var ratio = (double)cardsToTrash / cardsInDeck; if (cardsToTrash >= 5 && coinValue > 1.5 || cardsToTrash > 3 && ratio > 0.3 || cardsToTrash > 8) { // Also count any Capitals in play as 6 extra Debt tokens var debts = buyable.TopCard.BaseCost.Debt.Value + 6 * RealThis.InPlayAndSetAside[Cards.Empires.TypeClass.Capital].Count; score *= Math.Pow(ratio, 0.5) * Math.Pow(GameProgressLeft, 0.5 * (2 + Math.Max(0, debts - Currency.Coin.Value))); } else score = -1d; } } if (buyable.TopCard is Cards.Empires.Ritual) { if (!_Game.Table.Curse.CanGain()) score = -1d; if (Hand[c => c is Cards.DarkAges.Fortress || c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Rats].Any()) score = 6d; else if ( Hand[ c => c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains || (c is Cards.Hinterlands.BorderVillage && GameProgressLeft < 0.4) || (c is Cards.Hinterlands2ndEdition.BorderVillage && GameProgressLeft < 0.4) || c is Cards.Hinterlands.Farmland || c is Cards.Hinterlands2ndEdition.Farmland] .Any()) score = 5d; else if (Hand[c => c.Category.HasFlag(Categories.Ruins) || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece].Any()) score = 3d; else score = -1d; } if (buyable.TopCard is Cards.Empires.Tax) { var averageCardWorth = RealThis.SumAll(RealThis, c => true, c => c.BaseCost.Coin.Value + 2.5 * c.BaseCost.Potion.Value + 0.8 * c.BaseCost.Debt.Value) / RealThis.CountAll(); score *= 0.85 * Math.Pow(1.07, averageCardWorth); } if (buyable.TopCard is Cards.Empires.Triumph) { if (_Game.Table.Estate.CanGain()) score *= 0.85 * Math.Pow(1.1, (RealThis.CurrentTurn.CardsGained.Count + 1)); else score = -1d; } if (buyable.TopCard is Cards.Empires.Wedding) { if (!_Game.Table.Gold.CanGain()) score *= 0.5; else score *= 1.1 * GameProgressLeft; } if (buyable.TopCard is Cards.Empires.Windfall) { if (RealThis.DrawPile.Count > 0 || RealThis.DiscardPile.Count > 0) score = -1d; else score = 7; } if (buyable.TopCard is Cards.Renaissance.Silos) { // Don't buy Silos early game score *= 0.15 * Math.Min(5f, RealThis._Game.TurnsTaken.Count(t => t.Player == RealThis)) + 0.25; } if (buyable.TopCard is Cards.Menagerie.Commerce) { if (RealThis.CurrentTurn.CardsGained.Count == 0) score = -1; else score = Math.Pow(1.15, RealThis.CurrentTurn.CardsGained.DistinctBy(c => c.Name).Count()); } if (buyable.TopCard is Cards.Menagerie.Delay) { // If we don't have an Action card in hand we want to play, never buy this if (!RealThis.Hand.Any(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c))) score = -1; } if (buyable.TopCard is Cards.Menagerie.Desperation) { if (RealThis.CurrentTurn.CardsBought.Contains(buyable) || !RealThis._Game.Table.Curse.CanGain()) score = -1d; else { var desperationBuyables = RealThis._Game.Table.TableEntities.Values.OfType().Where(s => !(s is Cards.Menagerie.Desperation) && s.CanBuy(RealThis, RealThis.Currency) && ShouldBuy(s) ); // Yay recursion! var desperationScores = ValuateCardsToBuy(desperationBuyables); var bestScore = desperationScores.Keys.OrderByDescending(k => k).FirstOrDefault(); var previousBestScore = bestScore; // Now see what our best buy is if we gain a curse for +2 coins desperationBuyables = RealThis._Game.Table.TableEntities.Values.OfType().Where(s => !(s is Cards.Menagerie.Desperation) && s.CanBuy(RealThis, RealThis.Currency + new Currencies.Coin(2)) && ShouldBuy(s) ); desperationScores = ValuateCardsToBuy(desperationBuyables); bestScore = desperationScores.Keys.OrderByDescending(k => k).FirstOrDefault(); // All or nothing with Desperation! if (bestScore > 5 && bestScore > previousBestScore + 1.75) score = 99d; else score = -1; } } if (buyable.TopCard is Cards.Menagerie.March) { // If we don't have an Action card in our discard pile we want to play, never buy this if (!RealThis.DiscardPile.LookThrough(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c)).Any()) score = -1; } if (buyable.TopCard is Cards.Menagerie.Stampede) { // If we have 6+ cards in play, never buy this if (RealThis.InPlayAndSetAside.Count >= 6) score = -1; } if (buyable.TopCard is Cards.Menagerie.Toil) { // If we don't have an Action card in hand we want to play, never buy this if (!RealThis.Hand.Any(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c))) score = -1; } #region Landmark score modifiers if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Aqueduct) && buyable.TopCard.Category.HasFlag(Categories.Victory)) { // VP tokens on Aqueduct increase the value of gaining Victory cards score *= Math.Pow(1.25, _Game.Table.TableEntities[Cards.Empires.TypeClass.Aqueduct].Tokens.Count(t => t is Cards.Prosperity.VictoryToken)); } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.BanditFort) && (buyable.TopCard.Type == Cards.Universal.TypeClass.Silver || buyable.TopCard.Type == Cards.Universal.TypeClass.Gold)) { // Silver & Gold aren't great with Bandit Fort in play score *= Math.Sqrt(GameProgressLeft); } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Basilica) && (Currency - buyable.CurrentCost).Coin >= 2 && _Game.Table.TableEntities[Cards.Empires.TypeClass.Basilica].Tokens.Count(t => t is Cards.Prosperity.VictoryToken) >= 2) { // Cheaper cards are more appealing if we gain 2 VP from Basilica score *= 1.2; } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Battlefield) && buyable.TopCard.Category.HasFlag(Categories.Victory) && _Game.Table.TableEntities[Cards.Empires.TypeClass.Battlefield].Tokens.Count(t => t is Cards.Prosperity.VictoryToken) >= 2) { // Similar to Aqueduct, if Battlefield is out, gaining Victory cards is better score *= 1.2; } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Colonnade) && buyable.TopCard.Category.HasFlag(Categories.Action) && InPlayAndSetAside.Any(c => c.Type == buyable.TopCard.Type) && _Game.Table.TableEntities[Cards.Empires.TypeClass.Colonnade].Tokens.Count(t => t is Cards.Prosperity.VictoryToken) >= 2) { // Colonnade rewards us for buying Action cards we have in play score *= 1.2; } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.DefiledShrine) && buyable.TopCard.Type == Cards.Universal.TypeClass.Curse) { // VP tokens on Defiled Shrine make buying Curses worthwhile sometimes if (_Game.Table.TableEntities[Cards.Empires.TypeClass.DefiledShrine].Tokens.Count(t => t is Cards.Prosperity.VictoryToken) >= 3) score = 1.2 * Math.Pow(1.095, _Game.Table.TableEntities[Cards.Empires.TypeClass.DefiledShrine].Tokens.Count(t => t is Cards.Prosperity.VictoryToken)); } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Fountain) && buyable.TopCard.Type == Cards.Universal.TypeClass.Copper) { var copperCount = RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper, false); if (copperCount == 9) // Our 10th Copper reaps MASSIVE rewards if Fountain is in play score = 15; else if (copperCount < 9) // We can work towards it at least score = 0.25; } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Museum)) { // Museum wants us to have variety. Prizes, Knights, Castles, Ruins, Shelters are all good if (buyable.TopCard.Category.HasFlag(Categories.Knight) || buyable.TopCard.Category.HasFlag(Categories.Castle)) score *= 1.2; var supplyCount = RealThis.CountAll(RealThis, c => c.Type == buyable.TopCard.Type, false); if (GameProgressLeft < 0.5 && supplyCount == 0) score *= 1.1; else if (GameProgressLeft < 0.25 && supplyCount == 0) score *= 1.2; else if (GameProgressLeft < 0.5 && supplyCount >= 1) score *= 0.95; else if (GameProgressLeft < 0.25 && supplyCount >= 1) score *= 0.8; else if (supplyCount == 0) score *= 1.05; } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Obelisk) && buyable.Tokens.Any(t => t is Cards.Empires.ObeliskMarker)) { // Obelisk cards are a bit more valuable than normal score *= 1.25; } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Orchard) && buyable.TopCard.Category.HasFlag(Categories.Action) && RealThis.CountAll(RealThis, c => c.Type == buyable.TopCard.Type, false) == 2) { // Our 3rd Action card of a type is great if Orchard is in play score *= 1.5; } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Palace) && (buyable.TopCard.Type == Cards.Universal.TypeClass.Copper || buyable.TopCard.Type == Cards.Universal.TypeClass.Silver || buyable.TopCard.Type == Cards.Universal.TypeClass.Gold) ) { // Palace likes a balance of Copper / Silver / Gold var coppers = RealThis.CountAll(RealThis, c => c.Type == Cards.Universal.TypeClass.Copper, false); var silvers = RealThis.CountAll(RealThis, c => c.Type == Cards.Universal.TypeClass.Silver, false); var golds = RealThis.CountAll(RealThis, c => c.Type == Cards.Universal.TypeClass.Gold, false); if (buyable.TopCard.Type == Cards.Universal.TypeClass.Copper && (coppers < silvers || coppers < golds)) score = Math.Max(score * 1.4, 3.0); if (buyable.TopCard.Type == Cards.Universal.TypeClass.Silver && (silvers < coppers || silvers < golds)) score *= 1.4; if (buyable.TopCard.Type == Cards.Universal.TypeClass.Gold && (golds < coppers || golds < silvers)) score *= 1.4; } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.TriumphalArch) && buyable.TopCard.Category.HasFlag(Categories.Action)) { // For Triumphal Arch, we want to specialize in at least 2 Action types // This counts up how many of each Action card we have and prioritizes ones we already have a few of var cardsCount = new Dictionary>(); foreach (var type in CardsGained) { var count = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action) && c.Type == type, false); if (!cardsCount.ContainsKey(count)) cardsCount[count] = new List { type }; else cardsCount[count].Add(type); } var counts = cardsCount.Keys.OrderByDescending(k => k).Take(2).ToList(); if (counts.Any()) { if (cardsCount[counts[0]].Count >= 2 && cardsCount[counts[0]].Any(t => t == buyable.TopCard.Type)) score *= 1.1; if (cardsCount[counts[0]].Count == 1 && counts.Count > 1 && cardsCount[counts[1]].Any(t => t == buyable.TopCard.Type)) score *= 1.4; } } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Wall)) { // Wall entices us to not buy trash // How to accomplish that? IDK // A good start is that Estates and other VP cards worth 1 VP are worthless // And 1 VP cantrips aren't a ton better (but at least they're not nothing) // Scale VP cards based on their value if (buyable.TopCard.Type == Cards.Universal.TypeClass.Estate) score = -1.0; else if (buyable.TopCard.Type == Cards.Intrigue.TypeClass.GreatHall || buyable.TopCard.Type == Cards.Intrigue2ndEdition.TypeClass.Mill) score *= 0.35; // Scale VP cards by the difference ratio of ((X-1)/X) ^ 0.5 else if (buyable.TopCard.Category.HasFlag(Categories.Victory) && buyable.TopCard is Card && ((Card)buyable.TopCard).VictoryPoints > 0) score *= Math.Sqrt(((double)((Card)buyable.TopCard).VictoryPoints - 1) / ((Card)buyable.TopCard).VictoryPoints); // It better be worth it to buy this card else if (GameProgressLeft < 0.5 && RealThis.CountAll(RealThis) >= 15) score *= 0.75 * Math.Sqrt(GameProgressLeft); } if (_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.WolfDen)) { // Wolf Den wants us to not have variety. Prizes, Knights, Castles, Ruins, Shelters are all bad if (buyable.TopCard.Category.HasFlag(Categories.Knight) || buyable.TopCard.Category.HasFlag(Categories.Castle)) score *= 0.4; var supplyCount = RealThis.CountAll(RealThis, c => c.Type == buyable.TopCard.Type, false); if (GameProgressLeft < 0.5 && supplyCount == 0) score *= 0.85; if (GameProgressLeft < 0.25 && supplyCount == 0) score *= 0.35; if (GameProgressLeft < 0.5 && supplyCount == 1) score *= 1.15; if (GameProgressLeft < 0.25 && supplyCount == 1) score *= 1.5; } #endregion if (buyable.TopCard.Traits.HasFlag(Traits.Terminal) || buyable.TopCard.Traits.HasFlag(Traits.PlusMultipleActions)) { var totalCards = RealThis.CountAll(); float terminals = RealThis.CountAll(RealThis, c => c.Traits.HasFlag(Traits.Terminal)); float extraActors = RealThis.SumAll(RealThis, c => c.Traits.HasFlag(Traits.PlusMultipleActions), c => c is Card ? ((Card)c).Benefit.Actions - 1 + ((Card)c).DurationBenefit.Actions : 0); // Add in extra Village enablers extraActors += RealThis.CountAll(RealThis, c => c is Cards.Cornucopia.Hamlet || c is Cards.Cornucopia.TrustySteed || c is Cards.Cornucopia2ndEdition.Hamlet || c is Cards.Cornucopia2ndEdition.TrustySteed || c is Cards.DarkAges.Squire || c is Cards.DarkAges2ndEdition.Squire || c is Cards.Intrigue.Nobles || c is Cards.Intrigue2ndEdition.Nobles || c is Cards.Adventures.CoinOfTheRealm || c is Cards.Adventures.RoyalCarriage || c is Cards.Adventures2ndEdition.CoinOfTheRealm || c is Cards.Adventures2ndEdition.RoyalCarriage || c is Cards.Nocturne.GhostTown // Ghost Town is a delayed Village ); extraActors += 2 * RealThis.CountAll(RealThis, c => c is Cards.Adventures.CoinOfTheRealm || c is Cards.Adventures2ndEdition.CoinOfTheRealm); var crossroads = RealThis.CountAll(RealThis, c => c is Cards.Hinterlands.Crossroads || c is Cards.Hinterlands2ndEdition.Crossroads); // Probability that we'll have a hand with exactly 0 Crossroads var prob_0 = PermutationsAndCombinations.nCr(totalCards - crossroads, 5) / (double)PermutationsAndCombinations.nCr(totalCards, 5); // Probability that we'll have a hand with exactly 1 Crossroads var prob_1 = (PermutationsAndCombinations.nCr(totalCards - crossroads, 4) * crossroads) / (double)PermutationsAndCombinations.nCr(totalCards, 5); // This multiplies the number of Crossroads in our deck times the probability that we'll have a hand with 0 or one in our deck and then randomly rounds it up or down // If we get a hand with 2 or more Crossroads, that effectively means we only have Village. // Finally, we multiply by 2 since the first Crossroads adds +3 actions (Super Village!) var logicalCrossroads = (int)(crossroads * (prob_0 + prob_1) + _Game.RNG.NextDouble() / 2); extraActors += 2 * logicalCrossroads; // All the rest count as Terminals terminals += crossroads - logicalCrossroads; // Tributes count as ~1/3 of a Village (hard to gauge) & Terminals the rest var tributes = RealThis.CountAll(RealThis, c => c is Cards.Intrigue.Tribute); var logicalTributes = tributes / 3f; extraActors += logicalTributes; terminals += tributes - logicalTributes; // Encampment counts as ~1/2 of a Village (since it will easily go away) var encampments = RealThis.CountAll(RealThis, c => c is Cards.Empires.Encampment); extraActors -= encampments / 2f; // Conclave and Imp aren't exactly Terminals var conclaves = RealThis.CountAll(RealThis, c => c is Cards.Nocturne.Conclave); var imps = RealThis.CountAll(RealThis, c => c is Cards.Nocturne.Imp); terminals -= (0.75f * conclaves + 0.25f * imps); if (extraActors > 0) { extraActors += RealThis.CountAll(RealThis, c => c is Cards.Base.ThroneRoom || c is Cards.Base2ndEdition.ThroneRoom || c is Cards.DarkAges.Procession || c is Cards.DarkAges2019Errata.Procession || c is Cards.Adventures.Disciple || c is Cards.Empires.Crown); extraActors += 2 * RealThis.CountAll(RealThis, c => c is Cards.Prosperity.KingsCourt || c is Cards.Prosperity2ndEdition.KingsCourt || c is Cards.Menagerie.Mastermind); } // Champion gives us all the actions we would ever need if (RealThis.CountAll(RealThis, c => c is Cards.Adventures.Champion || c is Cards.Adventures2ndEdition.Champion) > 0) extraActors = 1000; // Try to balance out Villages and terminal actions var terminalGlut = terminals - extraActors; if (terminalGlut > 0 && buyable.TopCard.Traits.HasFlag(Traits.Terminal) || terminalGlut < 0 && buyable.TopCard.Traits.HasFlag(Traits.PlusMultipleActions)) score *= Math.Pow(0.7, 5 * terminalGlut / (double)totalCards); else if (terminalGlut < 0 && buyable.TopCard.Traits.HasFlag(Traits.Terminal) || terminalGlut > 0 && buyable.TopCard.Traits.HasFlag(Traits.PlusMultipleActions)) score *= Math.Pow(1.05, 5 * -terminalGlut / (double)totalCards); } if (!scores.ContainsKey(score)) scores[score] = new List(); scores[score].Add(buyable); } Console.WriteLine("Scores:"); foreach (var item in scores.OrderBy(kvp => kvp.Key)) { var sb = new StringBuilder(); sb.AppendFormat("{0:0.00}: ", item.Key); var first = true; foreach (var buyable in item.Value) { if (!first) sb.Append(", "); sb.Append(buyable.Name); first = false; } Console.WriteLine(sb.ToString()); } return scores; } protected override ISupply FindBestCardForCost(IEnumerable buyableSupplies, Currency currency, bool buying) { var bestSupplies = new List(); var bestCost = -1.0; var buyableSuppliesList = buyableSupplies as IList ?? buyableSupplies.ToList(); foreach (var supply in buyableSuppliesList) { if (!ShouldBuy(supply)) continue; // Only return ones we CAN gain if (currency != (Currency)null && currency < supply.CurrentCost) continue; // Overpay cards are, by default, worse than their initial cost var supplyCost = (supply.TopCard.BaseCost.Coin.Value + 2.5 * supply.TopCard.BaseCost.Potion.Value + 1.2 * supply.TopCard.BaseCost.Debt.Value) * (supply.BaseCost.CanOverpay ? 0.75 : 1.0); if (bestCost < 0.0 || bestCost <= supplyCost) { // Don't buy if it's not a Victory card near the end of the game if (GameProgressLeft < 0.25 && !supply.TopCard.Category.HasFlag(Categories.Victory)) continue; // Never buy Duchies or Estates early if ((GameProgressLeft > 0.25 && supply.Type == Cards.Universal.TypeClass.Estate) || (GameProgressLeft > 0.25 && supply.Type == Cards.Alchemy.TypeClass.Vineyard) || (GameProgressLeft > 0.35 && supply.Type == Cards.Base.TypeClass.Gardens) || (GameProgressLeft > 0.35 && supply.Type == Cards.Base2ndEdition.TypeClass.Gardens) || (GameProgressLeft > 0.35 && supply.Type == Cards.Hinterlands.TypeClass.SilkRoad) || (GameProgressLeft > 0.35 && supply.Type == Cards.Hinterlands2ndEdition.TypeClass.SilkRoad) || (GameProgressLeft > 0.35 && supply.Type == Cards.DarkAges.TypeClass.Feodum) || (GameProgressLeft > 0.35 && supply.Type == Cards.DarkAges2ndEdition.TypeClass.Feodum) || (GameProgressLeft > 0.4 && supply.Type == Cards.Universal.TypeClass.Duchy) || (GameProgressLeft > 0.4 && supply.Type == Cards.Intrigue.TypeClass.Duke) || (GameProgressLeft > 0.4 && supply.Type == Cards.Cornucopia.TypeClass.Fairgrounds) || (GameProgressLeft > 0.4 && supply.Type == Cards.Cornucopia2ndEdition.TypeClass.Fairgrounds) ) continue; // Never buy Distant Lands very late if (GameProgressLeft < 0.20 && supply.Type == Cards.Adventures.TypeClass.DistantLands) continue; // Duke/Duchy decision if (supply.TableableType == Cards.Intrigue.TypeClass.Duke || supply.TableableType == Cards.Universal.TypeClass.Duchy) { var duchies = RealThis.CountAll(RealThis, c => c is Cards.Universal.Duchy, false); var dukes = RealThis.CountAll(RealThis, c => c is Cards.Intrigue.Duke, false); // If gaining a Duke is not as useful as gaining a Duchy, don't get the Duke if (supply.TableableType == Cards.Intrigue.TypeClass.Duke && duchies - dukes < 4) continue; // If gaining a Duchy is not as useful as gaining a Duke, don't get the Duchy if (supply.TableableType == Cards.Universal.TypeClass.Duchy && duchies - dukes >= 4) continue; } // Reset best cost to new one if (bestCost < 0.0 || bestCost < supplyCost) { bestCost = supplyCost; bestSupplies.Clear(); } bestSupplies.Add(supply); } } if (bestSupplies.Count == 0) { foreach (var supply in buyableSuppliesList) { if (supply.TableableType == Cards.Universal.TypeClass.Curse) continue; // Overpay cards are, by default, worse than their initial cost var supplyCost = (supply.BaseCost.Coin.Value + 2.5 * supply.BaseCost.Potion.Value) * (supply.BaseCost.CanOverpay ? 0.75 : 1.0); if (bestCost < 0.0 || bestCost <= supply.BaseCost.Coin.Value) { // Reset best cost to new one if (bestCost < 0.0 || bestCost < supplyCost) { bestCost = supplyCost; bestSupplies.Clear(); } bestSupplies.Add(supply); } } } if (bestSupplies.Count == 0) { return buyableSuppliesList.Any() ? buyableSuppliesList.ElementAt(_Game.RNG.Next(buyableSuppliesList.Count)) : null; } return bestSupplies[_Game.RNG.Next(bestSupplies.Count)]; } protected override ISupply FindWorstCardForCost(IEnumerable buyableSupplies, Currency currency) { var worstSupplies = new List(); var buyableSuppliesList = buyableSupplies as IList ?? buyableSupplies.ToList(); foreach (var supply in buyableSuppliesList) { // 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 buyableSuppliesList.ElementAt(_Game.RNG.Next(buyableSuppliesList.Count)); var worstSupply = worstSupplies.Find(s => s.Type == Cards.Universal.TypeClass.Curse); if (worstSupply != null) return worstSupply; worstSupply = worstSupplies.Find(s => s.Category.HasFlag(Categories.Ruins)); if (worstSupply != null) return worstSupply; return worstSupplies[_Game.RNG.Next(worstSupplies.Count)]; } protected override bool ShouldPlay(ICard card) { Contract.Requires(card != null, "card cannot be null"); if (!ShouldBuy(card.Type)) return false; var previousTurnIndex = RealThis._Game.TurnsTaken.Count - 2; Turn previousTurn = null; if (previousTurnIndex >= 0) previousTurn = RealThis._Game.TurnsTaken[previousTurnIndex]; if (card is Cards.Base.Library) { // Don't play this if we're already at 7 cards (after playing it, of course) if (RealThis.Hand.Count > 7) return false; } else if (card is Cards.Base.Mine || card is Cards.Base2ndEdition.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 (var treasureCard in RealThis.Hand[Categories.Treasure]) { var treasureCardCost = _Game.ComputeCost(treasureCard); if (RealThis._Game.Table.TableEntities.Values.OfType().Any( supply => supply.Any() && supply.TopCard.Category.HasFlag(Categories.Treasure) && supply.CurrentCost.Coin > treasureCardCost.Coin && supply.CurrentCost.Potion >= treasureCardCost.Potion)) return true; } return false; } else if (card is Cards.Base.Moneylender || card is Cards.Base2ndEdition.Moneylender) { // Don't play if no Copper cards in hand if (RealThis.Hand[Cards.Universal.TypeClass.Copper].Count == 0) return false; } // 1st edition Throne Room only (2nd edition makes it optional) else if (card is Cards.Base.ThroneRoom) { // Only play if there's at least 1 card in hand that we *can* play if (RealThis.Hand[Categories.Action].Any( c => ShouldBuy(c.Type) && c.Type != Cards.Base.TypeClass.ThroneRoom && c.Type != Cards.Base2ndEdition.TypeClass.ThroneRoom && c.Type != Cards.Prosperity.TypeClass.KingsCourt && c.Type != Cards.Prosperity2ndEdition.TypeClass.KingsCourt && c.Type != Cards.DarkAges.TypeClass.Procession //&& c.Type != Cards.DarkAges2019Errata.TypeClass.Procession && c.Type != Cards.Adventures.TypeClass.Disciple && c.Type != Cards.Adventures.TypeClass.RoyalCarriage && c.Type != Cards.Adventures2ndEdition.TypeClass.RoyalCarriage && c.Type != Cards.Empires.TypeClass.Crown && c.Type != Cards.Menagerie.TypeClass.Mastermind )) return true; return false; } else if (card is Cards.Seaside.Island || card is Cards.Seaside2ndEdition.Island) { // Only play Island if we have another Victory-only card in hand if (!RealThis.Hand.Any( c => (c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure)) || c.Category.HasFlag(Categories.Ruins) )) return false; } else if (card is Cards.Seaside.Outpost || card is Cards.Seaside2ndEdition.Outpost) { // Don't play if we're already in our 2nd turn if (previousTurn != null && previousTurn.Player == this) return false; } else if (card is Cards.Seaside.Tactician || card is Cards.Seaside2ndEdition.Tactician) { // Never play Tactician if there's one in play and we don't have anything to gain from playing another if (RealThis.SetAside[Cards.Seaside.TypeClass.Tactician].Any() && (RealThis.Hand.Count == 1 || RealThis.Currency.Coin + RealThis.Hand[Categories.Treasure].Sum(c => c.Benefit.Currency.Coin.Value) > 4 || RealThis.Hand[Categories.Action].Count > 2) ) return false; } else if (card is Cards.Seaside.SeaHag || card is Cards.Seaside2ndEdition.SeaHag) { // Useless to play if there aren't any Curses if (RealThis._Game.Table.Curse.Any()) return true; return false; } else if (card is Cards.Seaside.Smugglers || card is Cards.Seaside2ndEdition.Smugglers) { var playerToRight = RealThis._Game.GetPlayerFromIndex(this, -1); var mostRecentTurn = RealThis._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 && c.BaseCost.Debt == 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 is Cards.Prosperity.CountingHouse || card is Cards.Prosperity2ndEdition.CountingHouse) { if (RealThis.DiscardPile.LookThrough(c => c is Cards.Universal.Copper).Count == 0) return false; } else if (card is Cards.Prosperity.KingsCourt) { // Only play if there's at least 1 card in hand that we *can* play if (RealThis.Hand[Categories.Action].Any( c => ShouldBuy(c.Type) && c.Type != Cards.Base.TypeClass.ThroneRoom && c.Type != Cards.Base2ndEdition.TypeClass.ThroneRoom && c.Type != Cards.Prosperity.TypeClass.KingsCourt && c.Type != Cards.Prosperity2ndEdition.TypeClass.KingsCourt && c.Type != Cards.DarkAges.TypeClass.Procession //&& c.Type != Cards.DarkAges2019Errata.TypeClass.Procession && c.Type != Cards.Adventures.TypeClass.Disciple && c.Type != Cards.Adventures.TypeClass.RoyalCarriage && c.Type != Cards.Adventures2ndEdition.TypeClass.RoyalCarriage && c.Type != Cards.Empires.TypeClass.Crown && c.Type != Cards.Menagerie.TypeClass.Mastermind )) return true; return false; } else if (card is Cards.Prosperity.Mint) { foreach (var treasureCard in RealThis.Hand[Categories.Treasure]) { // We don't care about copying Copper cards if (treasureCard is Cards.Universal.Copper) continue; if (RealThis._Game.Table.TableEntities.ContainsKey(treasureCard) && ((ISupply)RealThis._Game.Table.TableEntities[treasureCard]).Any()) return true; } return false; } else if (card is Cards.Prosperity.Watchtower || card is Cards.Prosperity2ndEdition.Watchtower) { // Don't play this if we're already at 6 cards (after playing it, of course) if (RealThis.Hand.Count > 6) return false; } else if (card is Cards.Seaside.TreasureMap || card is Cards.Seaside2ndEdition.TreasureMap) { // Don't play Treasure Map if we've only got 1 in hand if (RealThis.Hand[Cards.Seaside.TypeClass.TreasureMap].Count <= 1 && RealThis.Hand[Cards.Seaside2ndEdition.TypeClass.TreasureMap].Count <= 1) return false; } else if (card is Cards.Adventures.Disciple) { // We almost always want to play this card, even if there is no benefit, because the real benefit is exchanging it for Teacher // The only time we don't want to play Disciple with no Action card to play is if we already have a Teacher in our deck if (RealThis.CountAll(RealThis, c => c is Cards.Adventures.Teacher) == 0) return true; return false; } else if (card is Cards.Empires.Crown) { // Only play if there's at least 1 card in hand that we *can* play if (RealThis.Hand[Categories.Action].Any( c => c.Type != Cards.Base.TypeClass.ThroneRoom && c.Type != Cards.Base2ndEdition.TypeClass.ThroneRoom && c.Type != Cards.Prosperity.TypeClass.KingsCourt && c.Type != Cards.Prosperity2ndEdition.TypeClass.KingsCourt && c.Type != Cards.DarkAges.TypeClass.Procession //&& c.Type != Cards.DarkAges2019Errata.TypeClass.Procession && c.Type != Cards.Adventures.TypeClass.Disciple && c.Type != Cards.Adventures.TypeClass.RoyalCarriage && c.Type != Cards.Adventures2ndEdition.TypeClass.RoyalCarriage && c.Type != Cards.Empires.TypeClass.Crown && c.Type != Cards.Menagerie.TypeClass.Mastermind && ShouldPlay(c) )) return true; return false; } else if (card is Cards.Empires.Temple) { if (RealThis.Hand[Cards.Universal.TypeClass.Curse].Any()) return true; if (RealThis.Hand[c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar].Any()) return true; var ruins = _Game.Table.FindSupplyPileByType(Cards.DarkAges.TypeClass.RuinsSupply, false); if (ruins != null && !ruins.Any() && RealThis.Hand[c => c is Cards.DarkAges.Marauder].Any()) return true; if (RealThis.Hand[c => c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece].Any() && ComputeAverageCoinValueInDeck(c => c.Category.HasFlag(Categories.Treasure)) > 1.5) return true; return false; } else if (card is Cards.Nocturne.Changeling) { var bestCardToGain = RealThis.InPlayAndSetAside.Select(c => new { Card = c, Value = ComputeValueInDeck(c) }).OrderByDescending(v => v.Value).FirstOrDefault(); if (bestCardToGain == null || bestCardToGain.Value < ComputeValueInDeck((ICost)card)) return false; } else if (card is Cards.Nocturne.Exorcist) { if (RealThis.Hand[Cards.Universal.TypeClass.Curse].Any()) return true; if (RealThis.Hand[c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar].Any()) return true; var ruins = _Game.Table.FindSupplyPileByType(Cards.DarkAges.TypeClass.RuinsSupply, false); if (ruins != null && !ruins.Any() && RealThis.Hand[c => c is Cards.DarkAges.Marauder].Any()) return true; return false; } else if (card is Cards.Menagerie.BountyHunter) { // We MUST Exile a card, so if doing so doesn't give us any benefit // (and we want it in our deck), then don't play it if (PlayerMats.ContainsKey(Cards.Menagerie.TypeClass.Exile) && RealThis.Hand.All(ch => // To avoid infinite recursion (ch is Cards.Menagerie.BountyHunter || ShouldPlay(ch)) && PlayerMats[Cards.Menagerie.TypeClass.Exile][ch.Name].Any() )) return false; // Also if the cards in hand give $3+, then don't play Bounty Hunter if (RealThis.Hand.All(ch => ch.Benefit.Currency.Coin >= 3)) 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) Fool if we have Lost in the Woods // 6) cheapest card left return cards.OrderByDescending(ComputeDiscardValue).Take(count); } /// /// Computes the value of discarding this card /// /// /// Value of discarding this card. Higher value means it has higher priority to discard private double ComputeDiscardValue(Card card) { // Currently, these values are arbitrary. This is another good candidate for evolving algorithms. if (card is Cards.Hinterlands.Tunnel || card is Cards.Hinterlands2ndEdition.Tunnel) return 100.0; if (card is Cards.Nocturne.FaithfulHound) return 90.0; if (card.Category.HasFlag(Categories.Victory) && !card.Category.HasFlag(Categories.Treasure) && !card.Category.HasFlag(Categories.Action)) return 85.0; if (card.Category.HasFlag(Categories.Curse)) return 80.0; if (card.Category.HasFlag(Categories.Ruins)) return 50.0; if (card is Cards.Nocturne.Fool && RealThis.Takeables.Any(s => s is Cards.Nocturne.LostInTheWoods)) return 40.0; // Generic worth of these cards is based on their cost // I need to figure out how to scale this properly return 45.0 - 5 * ComputeValueInDeck(card); } /// /// Computes the value of this card for its worth to be in the player's deck. This is usually just the cost, but not always. /// /// /// In-deck value of this card. Higher value means it has higher value for keeping around (sort-of the inverse of Trash value) private double ComputeValueInDeck(ICost card) { // Curse's value is considered -1.0 if (card is Cards.Universal.Curse) return -1.0; // Prize's cost of 0 makes this a slightly hard thing to judge, but 7 is a good starting point if (card.Category.HasFlag(Categories.Prize)) return 7.0; // I don't think Peddler is *worth* 8 coins -- its main power is with other cards seeing its cost of 8 coins if (card is Cards.Prosperity.Peddler) return 6.0; // The main utility of this card is its gain ability. Otherwise, it's only very slightly better than a normal Village if (card is Cards.Hinterlands.BorderVillage || card is Cards.Hinterlands2ndEdition.BorderVillage) return 3.2; // Once gained, Cache is (almost) just as good as a Gold if (card is Cards.Hinterlands.Cache || card is Cards.Hinterlands2ndEdition.Cache) return 5.9; // Once gained, Embassy's value goes up slightly if (card is Cards.Hinterlands.Embassy) return 5.3; // Farmland is tricky -- its main benefit is being trashed by other Farmlands or things like Remodel into Provinces if (card is Cards.Hinterlands.Farmland || card is Cards.Hinterlands2ndEdition.Farmland) return 4.5; // I consider this card less than a Silver after it's been gained if (card is Cards.Hinterlands.IllGottenGains || card is Cards.Hinterlands2ndEdition.IllGottenGains) return 2.25; // Inn's value drops slightly after being gained, because it's a weakish Village if (card is Cards.Hinterlands.Inn || card is Cards.Hinterlands2ndEdition.Inn) return 3.5; // Nomad Camp basically turns into a Woodcutter after being gained if (card is Cards.Hinterlands.NomadCamp || card is Cards.Hinterlands2ndEdition.NomadCamp) return 3.1; // Spice Merchant's value goes down as trashable Treasures diminish if (card is Cards.Hinterlands.SpiceMerchant || card is Cards.Hinterlands2ndEdition.SpiceMerchant) { var deckSize = RealThis.CountAll(RealThis); var trashables = RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper || cC is Cards.Prosperity.Loan || cC is Cards.Prosperity2ndEdition.Loan || cC is Cards.Guilds.Masterpiece || cC is Cards.Guilds2ndEdition.Masterpiece); var trashRatioInt = ((double)trashables) / deckSize * 3 - 1; // Complicated equation to be small for relatively low ratios of "bad" treasures & close to normal for relatively high ratios // In a starting deck with 11 cards, this returns 3.77 return 2 * (3 * trashRatioInt - 1) / (trashRatioInt + Math.Sqrt(14 * Math.Pow(trashRatioInt, 2) + 5)) + 2.5; } // Madman's pretty sweet -- basically like a delayed, when-you-want-it Tactician if (card is Cards.DarkAges.Madman || card is Cards.DarkAges2ndEdition.Madman) return 5.5; // I'm not entirely sure about the overall worth of Mercenary. I think for the Standard AI, it's low because it's a trasher if (card is Cards.DarkAges.Mercenary || card is Cards.DarkAges2ndEdition.Mercenary) return 3.0; // A one-shot Gold is probably worth about 5 -- just a guess though if (card is Cards.DarkAges.Spoils) return 5.0; // Doctor's value goes down slightly after being bought if (card is Cards.Guilds.Doctor || card is Cards.Guilds2ndEdition.Doctor) return 2.2; // Herald's value goes down slightly after being bought -- it's slightly better than a Village if (card is Cards.Guilds.Herald || card is Cards.Guilds2ndEdition.Herald) return 3.5; // Masterpiece turns into slightly better than a Copper after being bought if (card is Cards.Guilds.Masterpiece || card is Cards.Guilds2ndEdition.Masterpiece) return 1.15; // Stonemason's value goes down slightly after being bought if (card is Cards.Guilds.Stonemason || card is Cards.Guilds2ndEdition.Stonemason) return 1.5; // Lost City's value goes up (very) slightly after it's in your deck if (card is Cards.Adventures.LostCity) return 5.5; // Messenger's value goes down (very) slightly after it's in your deck if (card is Cards.Adventures.Messenger) return 3.75; // Port is a standard Village after it's been gained if (card is Cards.Adventures.Port || card is Cards.Adventures2ndEdition.Port) return 3.0; // The 8 Upgraded cards all have a real value of a bit higher than their stated costs (because of how hard it is to obtain them) if (card is Cards.Adventures.TreasureHunter || card is Cards.Adventures.Warrior || card is Cards.Adventures.Hero || card is Cards.Adventures.Champion || card is Cards.Adventures.Soldier || card is Cards.Adventures.Fugitive || card is Cards.Adventures.Disciple || card is Cards.Adventures.Teacher || card is Cards.Adventures2ndEdition.TreasureHunter || card is Cards.Adventures2ndEdition.Warrior || card is Cards.Adventures2ndEdition.Champion ) return card.BaseCost.Coin.Value + 0.9; // With no Curses left, some cards' values drop or rise considerably if (!RealThis._Game.Table.Curse.Any()) { // Witch turns into something weaker than Smithy with no Curses if (card is Cards.Base.Witch) return 3.5; // Sea Hag turns completely useless with no Curses if (card is Cards.Seaside.SeaHag || card is Cards.Seaside2ndEdition.SeaHag) return 0.0; // Familiar turns all but useless with no Curses if (card is Cards.Alchemy.Familiar) return 1.5; // Young Witch becomes very near worthless with no Curses if (card is Cards.Cornucopia.YoungWitch || card is Cards.Cornucopia2ndEdition.YoungWitch) return 2.3; // Swamp Hag becomes turns into a delayed Gold -- pretty bad -- with no Curses if (card is Cards.Adventures.SwampHag || card is Cards.Adventures2ndEdition.SwampHag) return 3.5; // Cursed Gold is basically a Gold after Curses are gone if (card is Cards.Nocturne.CursedGold) return 5.75; } // Cursed Gold is worse than its cost when there are still Curses around if (card is Cards.Nocturne.CursedGold) return 3.25; // This is the default Play value of each card. If nothing else is defined above, this is what is used return card.BaseCost.Coin.Value + 2.5 * card.BaseCost.Potion.Value + 1.2 * card.BaseCost.Debt.Value; } protected override IEnumerable FindBestCardsToTrash(IEnumerable cards, int count) { return FindBestCardsToTrash(cards, count, false); } protected IEnumerable FindBestCardsToTrash(IEnumerable cards, int count, bool onlyReturnTrashables) { // choose the worse card in hand in this order // 1) curse // 2) any ruins // 3) Sea Hag / Familiar if there are no curses left // 4) Loan if we have fewer than 3 Coppers left // 5) Copper (or Masterpiece) if we've got a lot of better Treasure // 6) Fortress // (If onlyReturnTrashables is false): // 7) lowest value from ComputeValueInDeck var cardsToTrash = new CardCollection(); var cardsList = (cards as IList ?? cards.ToList()).Where(c => !(c is Cards.Universal.Blank)); cardsToTrash.AddRange(cardsList.Where(c => c.Category.HasFlag(Categories.Curse)).Take(count)); if (cardsToTrash.Count >= count) return cardsToTrash; cardsToTrash.AddRange(cardsList.Where(c => c.Category.HasFlag(Categories.Ruins)).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; if (RealThis._Game.Table.Curse.Count <= 1) { cardsToTrash.AddRange(cardsList.Where(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar ).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; } cardsToTrash.AddRange(cardsList.Where(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; cardsToTrash.AddRange(cardsList.Where(c => c is Cards.DarkAges.Hovel).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; if (RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure) && (c.Benefit.Currency.Coin > 1 || c is Cards.Prosperity.Bank)) >= RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper)) { cardsToTrash.AddRange(cardsList.Where(c => c is Cards.Universal.Copper).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; } if (RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure) && (c.Benefit.Currency.Coin > 1 || c is Cards.Prosperity.Bank)) >= RealThis.CountAll(RealThis, c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece)) { cardsToTrash.AddRange(cardsList.Where(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; } if (RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 3) { cardsToTrash.AddRange(cardsList.Where(c => c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; } cardsToTrash.AddRange(cardsList.Where(c => c is Cards.DarkAges.Fortress).Take(count - cardsToTrash.Count)); if (cardsToTrash.Count >= count) return cardsToTrash; if (!onlyReturnTrashables) cardsToTrash.AddRange(cardsList.OrderBy(ComputeValueInDeck).ThenBy(c => c.Name).Where(c => !cardsToTrash.Contains(c)).Take(count - cardsToTrash.Count)); return cardsToTrash; } protected override IEnumerable FindBestCards(IEnumerable cards, int count) { // Choose the most expensive cards var cardsToReturn = new CardCollection(); cardsToReturn.AddRange(cards.OrderByDescending(c => c.BaseCost).ThenBy(c => c.Name).Take(count)); return cardsToReturn; } protected virtual bool IsCardOkForMeToDiscard(Card card) { Contract.Requires(card != null, "card cannot be null"); if (card.Category.HasFlag(Categories.Curse)) return true; if (card.Category.HasFlag(Categories.Ruins)) return true; if (card.Category.HasFlag(Categories.Victory) && !card.Category.HasFlag(Categories.Action) && !card.Category.HasFlag(Categories.Treasure)) return true; if (card is Cards.Universal.Copper) return true; if (card is Cards.Hinterlands.Tunnel || card is Cards.Hinterlands2ndEdition.Tunnel) return true; if (card is Cards.DarkAges.Hovel) return true; if (card is Cards.Nocturne.FaithfulHound) return true; return false; } protected virtual double ComputeAverageCoinValueInDeck(Predicate filter = null, bool onlyObtainable = true, bool onlyCurrentlyDrawable = false) { if (filter == null) filter = c => true; var cardCount = RealThis.SumAll(RealThis, filter, c => 1, onlyObtainable, onlyCurrentlyDrawable); var coinSum = RealThis.SumAll(RealThis, filter, card => { // All other overrides here if (card is Cards.Intrigue.Bridge) return ((Card)card).Benefit.Currency.Coin.Value + 1; if (card is Cards.Intrigue.Pawn) return 0.825; if (card is Cards.Intrigue.SecretChamber) return 2; if (card is Cards.Intrigue.Steward) return 1.625; if (card is Cards.Intrigue.TradingPost) return 1; if (card is Cards.Intrigue.Tribute) return 1.333; if (card is Cards.Seaside.PirateShip || card is Cards.Seaside2ndEdition.PirateShip) return 0.825 * RealThis.TokenPiles[Cards.Seaside.TypeClass.PirateShipToken].Count; if (card is Cards.Alchemy.PhilosophersStone || card is Cards.Alchemy2ndEdition.PhilosophersStone) return Math.Max(0, cardCount - 5); if (card is Cards.Prosperity.Bank) // Bank is hard to estimate return 4 * RealThis.SumAll(RealThis, c => c.Category.HasFlag(Categories.Treasure), c => 1, onlyObtainable, onlyCurrentlyDrawable) / (double)cardCount; if (card is Cards.Prosperity.City || card is Cards.Prosperity2ndEdition.City) return _Game.Table.EmptySupplyPiles > 1 ? 1 : 0; if (card is Cards.Prosperity.CountingHouse || card is Cards.Prosperity2ndEdition.CountingHouse) return RealThis.SumAll(RealThis, c => c is Cards.Universal.Copper, c => 1, onlyObtainable, onlyCurrentlyDrawable) / 3.0; if (card is Cards.Prosperity.Venture) // Need to do a recursive call on this??? Where does it end? return ((Card)card).Benefit.Currency.Coin.Value + ComputeAverageCoinValueInDeck(c => c.Category.HasFlag(Categories.Treasure) && !(c is Cards.Prosperity.Venture), onlyObtainable, onlyCurrentlyDrawable); if (card is Cards.Cornucopia.Diadem) // Just a guess... return ((Card)card).Benefit.Currency.Coin.Value + 1.4; if (card is Cards.Cornucopia.Harvest) return 2; if (card is Cards.Cornucopia.Princess) return 2; if (card is Cards.Hinterlands.FoolsGold) return 1 + 3 * 5 * RealThis.SumAll(RealThis, c => c is Cards.Hinterlands.FoolsGold, c => 1, onlyObtainable, onlyCurrentlyDrawable) / (double)cardCount; if (card is Cards.Hinterlands.Highway) return 1; if (card is Cards.DarkAges.Count || card is Cards.DarkAges2ndEdition.Count) return 2; if (card is Cards.DarkAges.PoorHouse || card is Cards.DarkAges2ndEdition.PoorHouse) return 2; if (card is Cards.Guilds.Baker || card is Cards.Guilds2ndEdition.Baker) return 1; if (card is Cards.Guilds.CandlestickMaker || card is Cards.Guilds2ndEdition.CandlestickMaker) return 1; if (card is Cards.Guilds.MerchantGuild || card is Cards.Guilds2ndEdition.MerchantGuild) return 1; if (card is Cards.Adventures.Amulet) return 0.333; if (card is Cards.Adventures.BridgeTroll || card is Cards.Adventures2ndEdition.BridgeTroll) return 1; if (card is Cards.Adventures.Giant || card is Cards.Adventures2ndEdition.Giant) return 3; if (card is Cards.Adventures.Miser) return 0.825 * RealThis.PlayerMats[Cards.Adventures.TypeClass.TavernMat].Count(c => c is Cards.Universal.Copper); if (card is Cards.Adventures.Soldier) return 1.05; if (card is Cards.Adventures.Storyteller || card is Cards.Adventures2ndEdition.Storyteller) return 0; if (card is Cards.Empires.Capital) // 6 "loan" coins that have to be paid back in the future. Hard to evaluate, but it's near 6 I think return 4.875; if (card is Cards.Empires.ChariotRace) return 0.5; if (card is Cards.Empires.Charm) return 1; if (card is Cards.Empires.FarmersMarket) return 2; if (card is Cards.Empires.Fortune) // No actual value, but "lots" return 6; if (card is Cards.Empires.OpulentCastle) return 2 * 5 * RealThis.SumAll(RealThis, c => c.Category.HasFlag(Categories.Victory), c => 1, onlyObtainable, onlyCurrentlyDrawable) / (double)cardCount; if (card is Cards.Empires.Settlers) return RealThis.SumAll(RealThis, c => c is Cards.Universal.Copper, c => 1, onlyObtainable, onlyCurrentlyDrawable) / 10.0; // Default return card is Card cCard ? cCard.Benefit.Currency.Coin.Value + 0.825 * cCard.DurationBenefit.Currency.Coin.Value : 0; }, onlyObtainable, onlyCurrentlyDrawable); return coinSum / cardCount; } private Func MultiplierExclusionPredicate() { return c => !(c is Cards.Base.Chapel) && !(c is Cards.Base.Library) && !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.SecretChamber) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Outpost) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.Tactician) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Outpost) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.Tactician) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.CountingHouse) && !(c is Cards.Prosperity.Forge) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity.Watchtower) && !(c is Cards.Prosperity2ndEdition.CountingHouse) && !(c is Cards.Prosperity2ndEdition.Forge) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Prosperity2ndEdition.Watchtower) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.JunkDealer) && !(c is Cards.DarkAges.Procession) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.DarkAges2019Errata.Procession) && !(c is Cards.Guilds.MerchantGuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.MerchantGuild) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Adventures.DistantLands) && !(c is Cards.Adventures.Duplicate) && !(c is Cards.Adventures.Champion) && !(c is Cards.Adventures.Teacher) && !(c is Cards.Adventures.RoyalCarriage) && !(c is Cards.Adventures2ndEdition.Champion) && !(c is Cards.Adventures2ndEdition.RoyalCarriage) && !(c is Cards.Empires.Sacrifice) && !(c is Cards.Nocturne.ZombieApprentice) && !(c is Cards.Nocturne.ZombieMason) && !(c is Cards.Menagerie.BountyHunter) && !(c is Cards.Menagerie.Displace) && !(c is Cards.Menagerie.Scrap) && !(c is Cards.Promotional.Dismantle) ; } private Func MultiplierDestructiveExclusionPredicate() { return c => !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Nocturne.ZombieApprentice) && !(c is Cards.Nocturne.ZombieMason) && !(c is Cards.Menagerie.Scrap) && !(c is Cards.Promotional.Dismantle) ; } protected override ChoiceResult DecideAttacked(Choice choice, AttackedEventArgs aea, IEnumerable cardsToReveal) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(aea != null, "aea cannot be null"); var toRevealList = cardsToReveal as IList ?? cardsToReveal.ToList(); // Always reveal my Moat if the attack hasn't been cancelled yet if (!aea.Cancelled && toRevealList.Contains(Cards.Base.TypeClass.Moat)) return new ChoiceResult(new CardCollection { aea.Revealable[Cards.Base.TypeClass.Moat].Card }); // Always reveal my Moat if the attack hasn't been cancelled yet if (!aea.Cancelled && toRevealList.Contains(Cards.Base2ndEdition.TypeClass.Moat)) return new ChoiceResult(new CardCollection { aea.Revealable[Cards.Base2ndEdition.TypeClass.Moat].Card }); // Always reveal my Secret Chamber if it hasn't been revealed yet if ((LastReactedItem == null || LastReactedItem != choice.Triggers[0]) && toRevealList.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 (toRevealList.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 (toRevealList.Contains(Cards.Cornucopia2ndEdition.TypeClass.HorseTraders)) return new ChoiceResult(new CardCollection { aea.Revealable[Cards.Cornucopia2ndEdition.TypeClass.HorseTraders].Card }); // Always reveal my Beggar if I can if (toRevealList.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.Type != Cards.Base.TypeClass.Thief && aea.AttackCard.Type != Cards.Seaside.TypeClass.PirateShip && aea.AttackCard.Type != Cards.Seaside2ndEdition.TypeClass.PirateShip && aea.AttackCard.Type != Cards.Hinterlands.TypeClass.NobleBrigand && aea.AttackCard.Type != Cards.Hinterlands2ndEdition.TypeClass.NobleBrigand && ((aea.AttackCard.Type != Cards.Cornucopia.TypeClass.YoungWitch && aea.AttackCard.Type != Cards.Cornucopia2ndEdition.TypeClass.YoungWitch) || RealThis._Game.Table.TableEntities[Cards.DarkAges.TypeClass.Beggar].Tokens .All(t => t is Cards.Cornucopia.BaneMarker))) { return new ChoiceResult(new CardCollection { aea.Revealable[Cards.DarkAges.TypeClass.Beggar].Card }); } } // Always reveal my Beggar if I can if (toRevealList.Contains(Cards.DarkAges2ndEdition.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.Type != Cards.Base.TypeClass.Thief && aea.AttackCard.Type != Cards.Seaside.TypeClass.PirateShip && aea.AttackCard.Type != Cards.Seaside2ndEdition.TypeClass.PirateShip && aea.AttackCard.Type != Cards.Hinterlands.TypeClass.NobleBrigand && aea.AttackCard.Type != Cards.Hinterlands2ndEdition.TypeClass.NobleBrigand && ((aea.AttackCard.Type != Cards.Cornucopia.TypeClass.YoungWitch && aea.AttackCard.Type != Cards.Cornucopia2ndEdition.TypeClass.YoungWitch) || RealThis._Game.Table.TableEntities[Cards.DarkAges.TypeClass.Beggar].Tokens .All(t => t is Cards.Cornucopia.BaneMarker))) { return new ChoiceResult(new CardCollection { aea.Revealable[Cards.DarkAges2ndEdition.TypeClass.Beggar].Card }); } } // Always play my Caravan Guard if I can if (toRevealList.Contains(Cards.Adventures.TypeClass.CaravanGuard)) return new ChoiceResult(new CardCollection { aea.Revealable[Cards.Adventures.TypeClass.CaravanGuard].Card }); // Always play my Caravan Guard if I can if (toRevealList.Contains(Cards.Adventures2ndEdition.TypeClass.CaravanGuard)) return new ChoiceResult(new CardCollection { aea.Revealable[Cards.Adventures2ndEdition.TypeClass.CaravanGuard].Card }); // Always reveal my Diplomat if I can if (toRevealList.Contains(Cards.Intrigue2ndEdition.TypeClass.Diplomat)) return new ChoiceResult(new CardCollection { aea.Revealable[Cards.Intrigue2ndEdition.TypeClass.Diplomat].Card }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideCardBuy(Choice choice, CardBuyEventArgs cbea, IEnumerable triggerTypes) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(cbea != null, "cbea cannot be null"); var cardTriggerTypes = triggerTypes as IList ?? triggerTypes.ToList(); var type = 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.Prosperity2ndEdition.TypeClass.Talisman || t == Cards.Hinterlands.TypeClass.Farmland || t == Cards.Hinterlands.TypeClass.Haggler || t == Cards.Hinterlands.TypeClass.NobleBrigand || t == Cards.Hinterlands2ndEdition.TypeClass.Farmland || t == Cards.Hinterlands2ndEdition.TypeClass.Haggler || t == Cards.Hinterlands2ndEdition.TypeClass.NobleBrigand || t == Cards.DarkAges.TypeClass.Hovel || t == Cards.Guilds.TypeClass.Doctor || t == Cards.Guilds.TypeClass.Herald || t == Cards.Guilds.TypeClass.Masterpiece || t == Cards.Guilds.TypeClass.Stonemason || t == Cards.Guilds2ndEdition.TypeClass.Doctor || t == Cards.Guilds2ndEdition.TypeClass.Herald || t == Cards.Guilds2ndEdition.TypeClass.Masterpiece || t == Cards.Guilds2ndEdition.TypeClass.Stonemason || t == Cards.Adventures.TypeClass.HauntedWoods || t == Cards.Adventures.TypeClass.Messenger || t == Cards.Adventures.TypeClass.Port || t == Cards.Adventures.TypeClass.SwampHag || t == Cards.Adventures.TypeClass.TrashingToken || t == Cards.Adventures2ndEdition.TypeClass.HauntedWoods || t == Cards.Adventures2ndEdition.TypeClass.Port || t == Cards.Adventures2ndEdition.TypeClass.SwampHag || t == Cards.Empires.TypeClass.Forum || t == Cards.Empires.TypeClass.Basilica || t == Cards.Empires.TypeClass.Colonnade || t == Cards.Empires.TypeClass.DefiledShrine || t == Cards.Empires.TypeClass.Charm ); if (type != null) return new ChoiceResult(new List { cbea.Resolvers[type].Text }); return base.DecideCardBuy(choice, cbea, cardTriggerTypes); } protected override ChoiceResult DecideCardFollowingInstructions(Choice choice, CardFollowingInstructionsEventArgs cfiea, IEnumerable optionsToResolve) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(cfiea != null, "cfiea cannot be null"); // This is a really hard decision for most of the Ways, so in general, don't do them // The following are strictly better than Necropolis, any Ruins, or any card we don't want to play, so do it bool badCardWayMatch(Type t) => t == Cards.Menagerie.TypeClass.WayOfTheButterfly || t == Cards.Menagerie.TypeClass.WayOfTheCamel || t == Cards.Menagerie.TypeClass.WayOfTheHorse || t == Cards.Menagerie.TypeClass.WayOfTheMole || t == Cards.Menagerie.TypeClass.WayOfTheMonkey || t == Cards.Menagerie.TypeClass.WayOfTheMule || t == Cards.Menagerie.TypeClass.WayOfTheOtter || (t == Cards.Menagerie.TypeClass.WayOfTheOwl && RealThis.Hand.Count < 6) || t == Cards.Menagerie.TypeClass.WayOfTheOx || t == Cards.Menagerie.TypeClass.WayOfThePig || t == Cards.Menagerie.TypeClass.WayOfTheSeal || t == Cards.Menagerie.TypeClass.WayOfTheSheep || t == Cards.Menagerie.TypeClass.WayOfTheSquirrel || t == Cards.Menagerie.TypeClass.WayOfTheTurtle || (t == Cards.Menagerie.TypeClass.WayOfTheWorm && _Game.Table.Estate.CanGain()) || (t == Cards.Menagerie.TypeClass.WayOfTheGoat && FindBestCardsToTrash(RealThis.Hand, 1, true).Any()); if ((cfiea.Card.Category.HasFlag(Categories.Ruins) || cfiea.Card is Cards.DarkAges.Necropolis || !ShouldPlay(cfiea.Card)) && optionsToResolve.Any(badCardWayMatch)) return new ChoiceResult(new List { cfiea.Resolvers[cfiea.Resolvers.Keys.First(badCardWayMatch)].Text }); // If Way of the Owl's card draw is more than what the card offers, then resolve as Way of the Owl bool owlMatch(Type t) => t == Cards.Menagerie.TypeClass.WayOfTheOwl; if (( cfiea.Card is Cards.Base.Smithy || (cfiea.Card is Cards.Base.Witch && !_Game.Table.Curse.CanGain()) || (cfiea.Card is Cards.Base2ndEdition.Witch && !_Game.Table.Curse.CanGain()) || cfiea.Card is Cards.DarkAges.HuntingGrounds || cfiea.Card is Cards.Nocturne.FaithfulHound || (cfiea.Card is Cards.Nocturne.Werewolf && (_Game.ActivePlayer.Phase == PhaseEnum.Action || _Game.ActivePlayer.Phase == PhaseEnum.ActionTreasure)) || cfiea.Card is Cards.Renaissance.Lackeys || (cfiea.Card is Cards.Menagerie.BlackCat && _Game.ActivePlayer == RealThis) || cfiea.Card is Cards.Menagerie.Sheepdog ) && cfiea.Card.Benefit.Cards < 6 - RealThis.Hand.Count && optionsToResolve.Any(owlMatch)) return new ChoiceResult(new List { cfiea.Resolvers[cfiea.Resolvers.Keys.First(owlMatch)].Text }); // If our hand is worse than what we could draw, then resolve as Way of the Mole bool moleMatch(Type t) => t == Cards.Menagerie.TypeClass.WayOfTheMole; if (optionsToResolve.Any(moleMatch)) { var totalDeckValue = RealThis.SumAll(RealThis, c => true, c => c is Card cCard ? ComputeValueInDeck(cCard) : 0); var averageDeckValue = totalDeckValue / RealThis.CountAll(); // Factor in discard-for-benefit cards that are in our hand if (3 * averageDeckValue > RealThis.Hand.Sum(ComputeValueInDeck) - 0.75 * RealThis.Hand.Count(c => c is Cards.Hinterlands.Tunnel || c is Cards.Hinterlands2ndEdition.Tunnel || c is Cards.Nocturne.FaithfulHound || c is Cards.Menagerie.VillageGreen )) return new ChoiceResult(new List { cfiea.Resolvers[cfiea.Resolvers.Keys.First(moleMatch)].Text }); } var required = choice.Options.FirstOrDefault(o => o.IsRequired); if (required != null) return new ChoiceResult(new List { required.Text }); // Don't play as a Way return new ChoiceResult(new List()); } protected override ChoiceResult DecideCardGain(Choice choice, CardGainEventArgs cgea, IEnumerable triggerTypes) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(cgea != null, "cgea cannot be null"); var triggerTypesList = triggerTypes as IList ?? triggerTypes.ToList(); // Always reveal & trash this if we don't have 2 or more in hand bool foolsGold(string s) => choice.Options.Any(o => o.Text == s) && (s == Cards.Hinterlands.TypeClass.FoolsGold.ToString() || s == Cards.Hinterlands2ndEdition.TypeClass.FoolsGold.ToString() ); if (choice.PlayerSource != this && triggerTypesList.Any(foolsGold)) { if (RealThis.Hand[Cards.Hinterlands.TypeClass.FoolsGold].Count < 2) { var key = cgea.Resolvers.Keys.First(foolsGold); return new ChoiceResult(new List { cgea.Resolvers[key].Text }); } } // Always reveal Trader when Gaining a Curse or Copper -- Silver (or even nothing) is better anyway // Also do the same for Ruins & Masterpiece // This should happen before Watchtower -- we'll assume that gaining a Silver is better // than trashing the Curse or Copper bool regainers(string s) => choice.Options.Any(o => o.Text == s) && (s == Cards.Hinterlands.TypeClass.Trader.ToString() || s == Cards.Hinterlands2ndEdition.TypeClass.Trader.ToString() ); if (triggerTypesList.Any(regainers)) { if (choice.Triggers[0].Type == Cards.Universal.TypeClass.Curse || choice.Triggers[0].Type == Cards.Universal.TypeClass.Copper || choice.Triggers[0].Category.HasFlag(Categories.Ruins) || choice.Triggers[0].Type == Cards.Guilds.TypeClass.Masterpiece || choice.Triggers[0].Type == Cards.Guilds2ndEdition.TypeClass.Masterpiece) { var key = cgea.Resolvers.Keys.First(regainers); return new ChoiceResult(new List { cgea.Resolvers[key].Text }); } } // Always reveal Falconer and Black Cat & Sheepdog if it's not our buy phase bool gainPlayers(string s) => choice.Options.Any(o => o.Text == s) && (s == Cards.Menagerie.TypeClass.Falconer.ToString() || (s == Cards.Menagerie.TypeClass.BlackCat.ToString() && RealThis.Phase != PhaseEnum.Buy) || (s == Cards.Menagerie.TypeClass.Sheepdog.ToString() && RealThis.Phase != PhaseEnum.Buy) ); if (triggerTypesList.Any(gainPlayers)) { var key = cgea.Resolvers.Keys.First(gainPlayers); return new ChoiceResult(new List { cgea.Resolvers[key].Text }); } bool topDeckers(string s) => choice.Options.Any(o => o.Text == s) && (s == Cards.Prosperity.TypeClass.RoyalSeal.ToString() || s == Cards.Prosperity2ndEdition.TypeClass.RoyalSeal.ToString() || s == Cards.Adventures.TypeClass.TravellingFair.ToString() || s == Cards.Adventures2ndEdition.TypeClass.TravellingFair.ToString() || s == Cards.Nocturne.TypeClass.Tracker.ToString() || s == Cards.Menagerie.TypeClass.Sleigh.ToString() ); // Always put card on top of your deck if (triggerTypesList.Any(topDeckers)) { // Only put non-Action/non-Treasure Victory & non-Curse/Copper/Ruins/Ill-Gotten Gains/Doctor/Masterpiece/Stonemason on top of your deck if ((!choice.Triggers[0].Category.HasFlag(Categories.Victory) || choice.Triggers[0].Category.HasFlag(Categories.Action) || choice.Triggers[0].Category.HasFlag(Categories.Treasure)) && choice.Triggers[0].Type != Cards.Universal.TypeClass.Curse && choice.Triggers[0].Type != Cards.Universal.TypeClass.Copper && !choice.Triggers[0].Category.HasFlag(Categories.Ruins) && choice.Triggers[0].Type != Cards.Hinterlands.TypeClass.IllGottenGains && choice.Triggers[0].Type != Cards.Hinterlands.TypeClass.Tunnel && choice.Triggers[0].Type != Cards.Hinterlands2ndEdition.TypeClass.IllGottenGains && choice.Triggers[0].Type != Cards.Hinterlands2ndEdition.TypeClass.Tunnel && choice.Triggers[0].Type != Cards.Guilds.TypeClass.Doctor && choice.Triggers[0].Type != Cards.Guilds.TypeClass.Masterpiece && choice.Triggers[0].Type != Cards.Guilds.TypeClass.Stonemason && choice.Triggers[0].Type != Cards.Guilds2ndEdition.TypeClass.Doctor && choice.Triggers[0].Type != Cards.Guilds2ndEdition.TypeClass.Masterpiece && choice.Triggers[0].Type != Cards.Guilds2ndEdition.TypeClass.Stonemason ) { var key = cgea.Resolvers.Keys.First(topDeckers); return new ChoiceResult(new List { cgea.Resolvers[key].Text }); } } // Always reveal for Curse/Copper/Ruins cards from a Watchtower (to trash) // Never reveal for Victory cards/Ill-Gotten Gains/Tunnel/Masterpiece (the rest will go on top) bool trashers(string s) => choice.Options.Any(o => o.Text == s) && (s == Cards.Prosperity.TypeClass.Watchtower.ToString() || s == Cards.Prosperity2ndEdition.TypeClass.Watchtower.ToString() ); if (triggerTypesList.Any(trashers)) { if (choice.Triggers[0].Category != Categories.Victory && choice.Triggers[0].Category.HasFlag(Categories.Ruins) && choice.Triggers[0].Type != Cards.Universal.TypeClass.Curse && choice.Triggers[0].Type != Cards.Universal.TypeClass.Copper && choice.Triggers[0].Type != Cards.Hinterlands.TypeClass.IllGottenGains && choice.Triggers[0].Type != Cards.Hinterlands.TypeClass.Tunnel && choice.Triggers[0].Type != Cards.Hinterlands2ndEdition.TypeClass.IllGottenGains && choice.Triggers[0].Type != Cards.Hinterlands2ndEdition.TypeClass.Tunnel && choice.Triggers[0].Type != Cards.Guilds.TypeClass.Masterpiece && choice.Triggers[0].Type != Cards.Guilds2ndEdition.TypeClass.Masterpiece) { var key = cgea.Resolvers.Keys.First(trashers); return new ChoiceResult(new List { cgea.Resolvers[key].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 bool extraGainers(string s) => choice.Options.Any(o => o.Text == s) && (s == Cards.Hinterlands.TypeClass.Duchess.ToString() || s == Cards.Hinterlands2ndEdition.TypeClass.Duchess.ToString() ); if (triggerTypesList.Any(extraGainers)) { double duchessMultipleActionCards = RealThis.CountAll(RealThis, c => c.Traits.HasFlag(Traits.PlusMultipleActions)); double duchessTotalActionCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action)); double duchessDuchesses = RealThis.CountAll(RealThis, c => c is Cards.Hinterlands.Duchess || c is Cards.Hinterlands2ndEdition.Duchess); double duchessTotalCards = RealThis.CountAll(); if (duchessTotalActionCards / duchessTotalCards < 0.075 || duchessDuchesses / duchessTotalActionCards < 0.20 || duchessMultipleActionCards / duchessTotalActionCards > 0.33) { var key = cgea.Resolvers.Keys.First(extraGainers); return new ChoiceResult(new List { cgea.Resolvers[key].Text }); } } // Exile is a bit harder to manage. I guess always get back all copies of cards we want to play? bool exileMat(string s) => choice.Options.Any(o => o.Source.Type == Cards.Menagerie.TypeClass.Exile); if (triggerTypesList.Any(exileMat)) { var key = cgea.Resolvers.Keys.First(exileMat); // Don't ever discard Victory, Curse, or Copper cards from Exile if ((cgea.Card.Category.HasFlag(Categories.Victory) && !cgea.Card.Category.HasFlag(Categories.Action) && !cgea.Card.Category.HasFlag(Categories.Treasure) && !cgea.Card.Category.HasFlag(Categories.Night) ) || cgea.Card is Cards.Universal.Curse || cgea.Card is Cards.Universal.Copper ) { cgea.Resolvers.Remove(key); } else if (ShouldPlay(cgea.Card)) { return new ChoiceResult(new List { cgea.Resolvers[key].Text }); } } // Dunno what to do -- choose a random IsRequired one or nothing if there are none var requiredActions = cgea.Resolvers.Where(kvp => kvp.Value.IsRequired).ToList(); if (requiredActions.Any()) { var index = _Game.RNG.Next(requiredActions.Count); return new ChoiceResult(new List { cgea.Resolvers[requiredActions.ElementAt(index).Key].Text }); } return new ChoiceResult(new List()); } protected override ChoiceResult DecideCardPlayed(Choice choice, CardPlayedEventArgs cpea, IEnumerable cardTriggerTypes) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(cpea != null, "cpea cannot be null"); var triggerTypes = cardTriggerTypes as IList ?? cardTriggerTypes.ToList(); if (triggerTypes.Any(t => t == Cards.Adventures.TypeClass.CoinOfTheRealm)) { // If we don't have any Action remaining and we have at least one Action card in our hand that we want to play, Call Coin of the Realm if (ActionsAvailable() == 0 && RealThis.Hand.Any(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c))) return new ChoiceResult(new List { cpea.Resolvers.First(cpa => cpa.Value.Source is Cards.Adventures.CoinOfTheRealm).Value.Text }); } if (triggerTypes.Any(t => t == Cards.Adventures2ndEdition.TypeClass.CoinOfTheRealm)) { // If we don't have any Action remaining and we have at least one Action card in our hand that we want to play, Call Coin of the Realm if (ActionsAvailable() == 0 && RealThis.Hand.Any(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c))) return new ChoiceResult(new List { cpea.Resolvers.First(cpa => cpa.Value.Source is Cards.Adventures2ndEdition.CoinOfTheRealm).Value.Text }); } if (triggerTypes.Any(t => t == Cards.Promotional.TypeClass.Sauna)) { // If we have a card in hand we'd like to trash, resolve Sauna (to trash it) if (FindBestCardsToTrash(RealThis.Hand, 1, true).Any()) return new ChoiceResult(new List { cpea.Resolvers.First(cpa => cpa.Value.Source is Cards.Promotional.Sauna).Value.Text }); } return base.DecideCardPlayed(choice, cpea, triggerTypes); } protected override ChoiceResult DecideCardsDiscard(Choice choice, CardsDiscardEventArgs cdea, IEnumerable> triggerTypes) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(cdea != null, "cdea cannot be null"); var triggerTypesList = triggerTypes as IList> ?? triggerTypes.ToList(); // Always set aside Faithful Hound if I can if (triggerTypesList.Any(t => t.Item1 == Cards.Nocturne.TypeClass.FaithfulHound)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Nocturne.TypeClass.FaithfulHound).Text }); // Always put Treasury on my deck if I can if (triggerTypesList.Any(t => t.Item1 == Cards.Seaside.TypeClass.Treasury)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Seaside.TypeClass.Treasury).Text }); // Always put Treasury on my deck if I can if (triggerTypesList.Any(t => t.Item1 == Cards.Seaside2ndEdition.TypeClass.Treasury)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Seaside2ndEdition.TypeClass.Treasury).Text }); // Always put Alchemist on my deck if I can if (triggerTypesList.Any(t => t.Item1 == Cards.Alchemy.TypeClass.Alchemist)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Alchemy.TypeClass.Alchemist).Text }); // Always put Alchemist on my deck if I can if (triggerTypesList.Any(t => t.Item1 == Cards.Alchemy2ndEdition.TypeClass.Alchemist)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Alchemy2ndEdition.TypeClass.Alchemist).Text }); // Only perform Herbalist if there's at least 1 non-Copper Treasure card in play if (triggerTypesList.Any(t => t.Item1 == Cards.Alchemy.TypeClass.Herbalist) && RealThis.InPlayAndSetAside.Any(c => c.Category.HasFlag(Categories.Treasure) && !(c is Cards.Universal.Copper))) return new ChoiceResult(new List { cdea.GetResolver(Cards.Alchemy.TypeClass.Herbalist).Text }); // Only perform Herbalist if there's at least 1 non-Copper Treasure card in play if (triggerTypesList.Any(t => t.Item1 == Cards.Alchemy2ndEdition.TypeClass.Herbalist) && RealThis.InPlayAndSetAside.Any(c => c.Category.HasFlag(Categories.Treasure) && !(c is Cards.Universal.Copper))) return new ChoiceResult(new List { cdea.GetResolver(Cards.Alchemy2ndEdition.TypeClass.Herbalist).Text }); // Always reveal this when discarding if (triggerTypesList.Any(t => t.Item1 == Cards.Hinterlands.TypeClass.Tunnel)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Hinterlands.TypeClass.Tunnel).Text }); // Always reveal this when discarding if (triggerTypesList.Any(t => t.Item1 == Cards.Hinterlands2ndEdition.TypeClass.Tunnel)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Hinterlands2ndEdition.TypeClass.Tunnel).Text }); // Always exchange Peasant, Page, Treasure Hunter, & Soldier if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures.TypeClass.Peasant)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures.TypeClass.Peasant).Text }); if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures.TypeClass.Page)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures.TypeClass.Page).Text }); if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures.TypeClass.TreasureHunter)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures.TypeClass.TreasureHunter).Text }); if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures2ndEdition.TypeClass.TreasureHunter)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures2ndEdition.TypeClass.TreasureHunter).Text }); if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures.TypeClass.Soldier)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures.TypeClass.Soldier).Text }); // Exchange Warrior only if we don't have any Heros if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures.TypeClass.Warrior) && RealThis.CountAll(predicate: c => c is Cards.Adventures.Hero) < 1) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures.TypeClass.Warrior).Text }); // Exchange Warrior only if we don't have any Heros if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures2ndEdition.TypeClass.Warrior) && RealThis.CountAll(predicate: c => c is Cards.Adventures.Hero) < 1) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures2ndEdition.TypeClass.Warrior).Text }); // Exchange Hero only if we don't have any Champions if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures.TypeClass.Hero) && RealThis.CountAll(predicate: c => c is Cards.Adventures.Champion || c is Cards.Adventures2ndEdition.Champion) < 1 && RealThis.InPlayAndSetAside[Cards.Adventures.TypeClass.Champion].Count < 1 && RealThis.InPlayAndSetAside[Cards.Adventures2ndEdition.TypeClass.Champion].Count < 1) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures.TypeClass.Hero).Text }); // Exchange Fugitive only if we have 1 or fewer Disciples if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures.TypeClass.Fugitive) && RealThis.CountAll(predicate: c => c is Cards.Adventures.Disciple) < 2) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures.TypeClass.Fugitive).Text }); // Exchange Disciple only if we have no Teachers if (triggerTypesList.Any(t => t.Item1 == Cards.Adventures.TypeClass.Disciple) && RealThis.CountAll(predicate: c => c is Cards.Adventures.Teacher) < 1 && (!RealThis.PlayerMats.ContainsKey(Cards.Adventures.TypeClass.TavernMat) || !RealThis.PlayerMats[Cards.Adventures.TypeClass.TavernMat].Any(c => c is Cards.Adventures.Teacher))) return new ChoiceResult(new List { cdea.GetResolver(Cards.Adventures.TypeClass.Disciple).Text }); IEnumerable schemeOptions = cdea.Resolvers .Where(kvp => kvp.Key.Item1 == Cards.Hinterlands.TypeClass.Scheme || kvp.Key.Item1 == Cards.Hinterlands2ndEdition.TypeClass.Scheme) .Select(kvp => kvp.Value) .OrderBy(cda => ((Card)cda.Data).BaseCost.Coin.Value + 2.5f * ((Card)cda.Data).BaseCost.Potion.Value + 1.2f * ((Card)cda.Data).BaseCost.Debt.Value); if (schemeOptions.Any()) return new ChoiceResult(new List { schemeOptions.ElementAt(0).Text }); // Always play Village Green if I can if (triggerTypesList.Any(t => t.Item1 == Cards.Menagerie.TypeClass.VillageGreen)) return new ChoiceResult(new List { cdea.GetResolver(Cards.Menagerie.TypeClass.VillageGreen).Text }); // Dunno what to do -- choose a random IsRequired one or nothing if there are none var requiredActions = cdea.Resolvers.Where(a => a.Value.IsRequired).ToList(); if (requiredActions.Any()) { var index = _Game.RNG.Next(requiredActions.Count); return new ChoiceResult(new List { cdea.Resolvers[requiredActions.ElementAt(index).Key].Text }); } return new ChoiceResult(new List()); } protected override ChoiceResult DecideCleaningUp(Choice choice, CleaningUpEventArgs cuea, IEnumerable triggerTypes) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(cuea != null, "cuea cannot be null"); // Always choose a card with Scheme if I can (I should always be able to, yes?) var triggerTypesList = triggerTypes as IList ?? triggerTypes.ToList(); if (triggerTypesList.Contains(Cards.Hinterlands.TypeClass.Scheme)) return new ChoiceResult(new List { cuea.Resolvers[Cards.Hinterlands.TypeClass.Scheme].Text }); // Always choose a card with Scheme if I can (I should always be able to, yes?) triggerTypesList = triggerTypes as IList ?? triggerTypes.ToList(); if (triggerTypesList.Contains(Cards.Hinterlands2ndEdition.TypeClass.Scheme)) return new ChoiceResult(new List { cuea.Resolvers[Cards.Hinterlands2ndEdition.TypeClass.Scheme].Text }); // Always put Walled Village on my deck if I can if (triggerTypesList.Contains(Cards.Promotional.TypeClass.WalledVillage)) return new ChoiceResult(new List { cuea.Resolvers[Cards.Promotional.TypeClass.WalledVillage].Text }); // Always put Walled Village on my deck if I can if (triggerTypesList.Contains(Cards.Promotional2ndEdition.TypeClass.WalledVillage)) return new ChoiceResult(new List { cuea.Resolvers[Cards.Promotional2ndEdition.TypeClass.WalledVillage].Text }); return new ChoiceResult(new List()); } protected override ChoiceResult DecidePhaseChanging(Choice choice, PhaseChangingEventArgs pcea, IEnumerable cardTriggerTypes) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(pcea != null, "pcea cannot be null"); // Always retrieve Wine Merchant if we can var triggerTypesList = cardTriggerTypes as IList ?? cardTriggerTypes.ToList(); if (triggerTypesList.Contains(Cards.Adventures.TypeClass.WineMerchant)) return new ChoiceResult(new List { pcea.Resolvers[Cards.Adventures.TypeClass.WineMerchant].Text }); // Always discard an Action card for Arena if we can if (triggerTypesList.Contains(Cards.Empires.TypeClass.Arena)) return new ChoiceResult(new List { pcea.Resolvers[Cards.Empires.TypeClass.Arena].Text }); return base.DecidePhaseChanging(choice, pcea, triggerTypesList); } protected override ChoiceResult DecideTrash(Choice choice, TrashEventArgs tea, IEnumerable triggerTypes) { Contract.Requires(choice != null, "choice cannot be null"); Contract.Requires(tea != null, "tea cannot be null"); // Resolve Fortress first -- there's no reason not to var triggerTypesList = triggerTypes as IList ?? triggerTypes.ToList(); if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.Fortress)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.Fortress].Text }); // Always reveal Market Square when we can if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.MarketSquare)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.MarketSquare].Text }); // Always reveal Market Square when we can if (triggerTypesList.Contains(Cards.DarkAges2ndEdition.TypeClass.MarketSquare)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges2ndEdition.TypeClass.MarketSquare].Text }); // Resolve Sir Vander next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.SirVander)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.SirVander].Text }); // Resolve Sir Vander next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges2ndEdition.TypeClass.SirVander)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges2ndEdition.TypeClass.SirVander].Text }); // Resolve Feodum next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.Feodum)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.Feodum].Text }); // Resolve Feodum next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges2ndEdition.TypeClass.Feodum)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges2ndEdition.TypeClass.Feodum].Text }); // Resolve Squire next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.Squire)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.Squire].Text }); // Resolve Squire next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges2ndEdition.TypeClass.Squire)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges2ndEdition.TypeClass.Squire].Text }); // Resolve Catacombs next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.Catacombs)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.Catacombs].Text }); // Resolve Catacombs next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges2ndEdition.TypeClass.Catacombs)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges2ndEdition.TypeClass.Catacombs].Text }); // Resolve Rats next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.Rats)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.Rats].Text }); // Resolve Rats next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges2ndEdition.TypeClass.Rats)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges2ndEdition.TypeClass.Rats].Text }); // Resolve Overgrown Estate next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.OvergrownEstate)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.OvergrownEstate].Text }); // Resolve Overgrown Estate next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges2ndEdition.TypeClass.OvergrownEstate)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges2ndEdition.TypeClass.OvergrownEstate].Text }); // Resolve Cultist next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.Cultist)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.Cultist].Text }); // Resolve Cultist next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges2ndEdition.TypeClass.Cultist)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges2ndEdition.TypeClass.Cultist].Text }); // Resolve Hunting Grounds next -- not sure if any of these even matter if (triggerTypesList.Contains(Cards.DarkAges.TypeClass.HuntingGrounds)) return new ChoiceResult(new List { tea.Resolvers[Cards.DarkAges.TypeClass.HuntingGrounds].Text }); // Resolve Haunted Mirror if we can if (triggerTypesList.Contains(Cards.Nocturne.TypeClass.HauntedMirror)) return new ChoiceResult(new List { tea.Resolvers[Cards.Nocturne.TypeClass.HauntedMirror].Text }); // Dunno what to do -- choose a random IsRequired one or nothing if there are none var requiredActions = tea.Resolvers.Select(kvp => kvp.Value).Where(a => a.IsRequired).ToList(); if (requiredActions.Any()) { var index = _Game.RNG.Next(requiredActions.Count); return new ChoiceResult(new List { tea.Resolvers[requiredActions[index].Card.Type].Text }); } return new ChoiceResult(new List()); } protected override ChoiceResult DecideCardsReorder(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cards = new CardCollection(choice.Cards); // Order them in roughly random order cards.Shuffle(); return new ChoiceResult(cards); } protected override ChoiceResult DecideRevealBane(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always reveal the Bane card if I can return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideAdvance(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // Find worst Action card to trash var cardAdvance = FindBestCardsToTrash(choice.Cards, 1).FirstOrDefault(); if (cardAdvance == null || cardAdvance.BaseCost.Coin >= 6) return new ChoiceResult(new CardCollection()); return new ChoiceResult(new CardCollection { cardAdvance }); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } return base.DecideAdvance(choice); } protected override ChoiceResult DecideAdvisor(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Find most-expensive non-Victory card to discard // Focus only on Treasure cards if there are no Actions remaining Card cardAdvisor = null; foreach (var card in choice.Cards) { if ((card.Category.HasFlag(Categories.Treasure) || (ActionsAvailable(_Game.TurnsTaken.Last().Player) > 0 && card.Category.HasFlag(Categories.Action))) && (cardAdvisor == null || ComputeValueInDeck(card) > ComputeValueInDeck(cardAdvisor))) cardAdvisor = card; } if (cardAdvisor != null) return new ChoiceResult(new CardCollection { cardAdvisor }); return base.DecideAdvisor(choice); } protected override ChoiceResult DecideAlms(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideAltar(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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.OfType(), null, false)); default: return base.DecideAltar(choice); } } protected override ChoiceResult DecideAmbassador(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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].Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); default: return base.DecideAmbassador(choice); } } protected override ChoiceResult DecideAmulet(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // If we have a Curse or Ruins in hand, choose Trash. // If our Currency density is low, choose Gain Silver // Otherwise, choose +1 coin about 1/2 the time (Gain Silver the other half) if (RealThis.Hand[c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins)].Any()) return new ChoiceResult(new List { choice.Options[1].Text }); if (ComputeAverageCoinValueInDeck() < 1.5) // If Silver increases our deck's potency return new ChoiceResult(new List { choice.Options[2].Text }); if (new Random().Next() > 0.5) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[2].Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); default: return base.DecideAmulet(choice); } } protected override ChoiceResult DecideAnnex(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var annexToKeepInDiscard = new CardCollection(); // Never take Curses annexToKeepInDiscard.AddRange(choice.Cards.Where(c => c is Cards.Universal.Curse).Take(5)); // Never take Ruins if (annexToKeepInDiscard.Count < 5) annexToKeepInDiscard.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins)).Take(5 - annexToKeepInDiscard.Count)); // Never take Victory-only cards if (annexToKeepInDiscard.Count < 5) annexToKeepInDiscard.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure)).Take(5 - annexToKeepInDiscard.Count)); // Never take Tunnel (as fun as it is to discard) if (annexToKeepInDiscard.Count < 5) annexToKeepInDiscard.AddRange(choice.Cards.Where(c => c is Cards.Hinterlands.Tunnel || c is Cards.Hinterlands2ndEdition.Tunnel).Take(5 - annexToKeepInDiscard.Count)); // Never take cards we wouldn't voluntarily gain if (annexToKeepInDiscard.Count < 5) annexToKeepInDiscard.AddRange(choice.Cards.Where(c => !ShouldBuy(c.Type)).Take(5 - annexToKeepInDiscard.Count)); // Never take Copper if we can avoid it if (annexToKeepInDiscard.Count < 5) annexToKeepInDiscard.AddRange(choice.Cards.Where(c => c is Cards.Universal.Copper).Take(5 - annexToKeepInDiscard.Count)); return new ChoiceResult(annexToKeepInDiscard); } protected override ChoiceResult DecideApothecary(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var apothCards = new CardCollection(choice.Cards); // Order them in roughly random order apothCards.Shuffle(); return new ChoiceResult(apothCards); } protected override ChoiceResult DecideApprentice(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideArchive(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // This decision is incredibly situational and hard to evaluate. The general rule will be to always take the best card at the time return new ChoiceResult(new CardCollection { choice.Cards.OrderByDescending(ComputeValueInDeck).First() }); } protected override ChoiceResult DecideArmory(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Find the best non-Armory card first (that's a vicious cycle) var selector = choice.Supplies.Values.OfType().Where(s => !(s is Cards.DarkAges.Armory || s is Cards.DarkAges2ndEdition.Armory)); return new ChoiceResult(FindBestCardForCost(selector, null, false)); } protected override ChoiceResult DecideArtificer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // TODO -- need logic to figure out the ideal number of cards to discard. break; case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } return base.DecideArtificer(choice); } protected override ChoiceResult DecideArtisan(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); case ChoiceType.Cards: // This gets a bit harder. If we don't have any more Actions, put our best Action card back on top. // Otherwise, ... ? Put our best card back? IDK. // We need to assess coins available and what we want to do on a particular turn to decide which card should go back if (ActionsAvailable() == 0 && choice.Cards.Any(c => c.Category.HasFlag(Categories.Action))) return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards.Where(c => c.Category.HasFlag(Categories.Action)), 1))); return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); } return base.DecideArtisan(choice); } protected override ChoiceResult DecideAvanto(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always return "Yes" return new ChoiceResult(new List { choice.Options[0].Text }); // Yes } protected override ChoiceResult DecideBall(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideBandit(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Trash Masterpiece first var banditBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); // Coin of the Realm go next if (banditBestCard == null) banditBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Adventures.CoinOfTheRealm || c is Cards.Adventures2ndEdition.CoinOfTheRealm); // Loan goes next if (banditBestCard == null) banditBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan); // Humble Castle goes next if (banditBestCard == null) banditBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Empires.HumbleCastle); // Ill Gotten Gains goes next if (banditBestCard == null) banditBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains); // Talisman goes next if (banditBestCard == null) banditBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Talisman || c is Cards.Prosperity2ndEdition.Talisman); // Contraband goes next if (banditBestCard == null) banditBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Contraband); // Let's go cheapest next if (banditBestCard != null) return new ChoiceResult(new CardCollection { banditBestCard }); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideBandOfMisfits(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideBanish(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // TODO -- Decide which cards we should banish // Some early logic choices would be the most of Victory-only cards. // Curses are another good choice, assuming we have no way of getting rid of them easily break; case ChoiceType.Options: // Always choose to banish all copies return new ChoiceResult(choice.Options.Last().Text); } return base.DecideBanish(choice); } protected override ChoiceResult DecideBanquet(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideBargain(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideBarge(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (ActionsAvailable() > 1) // Always play for Now if we've got 2+ Actions left return new ChoiceResult(new List { choice.Options[0].Text }); return base.DecideBarge(choice); } protected override ChoiceResult DecideBaron(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always discard an Estate if I can return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideBat(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cardsToTrash = new List(); cardsToTrash.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Curse)).Take(2)); if (cardsToTrash.Count < 2) cardsToTrash.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins)).Take(2 - cardsToTrash.Count)); if (cardsToTrash.Count < 2) cardsToTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Hovel).Take(2 - cardsToTrash.Count)); // Trash trashers we won't play if (cardsToTrash.Count < 1) cardsToTrash.AddRange(choice.Cards.Where(c => (c is Cards.Base.Moneylender && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || c is Cards.Base.Remodel || (c is Cards.Base2ndEdition.Moneylender && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || c is Cards.Intrigue.TradingPost || c is Cards.Intrigue.Upgrade || c is Cards.Seaside.Ambassador || c is Cards.Seaside.Lookout || c is Cards.Seaside.Salvager || c is Cards.Seaside2ndEdition.Ambassador || c is Cards.Seaside2ndEdition.Lookout || c is Cards.Seaside2ndEdition.Salvager || c is Cards.Prosperity.Expand || c is Cards.Prosperity.Forge || c is Cards.Prosperity2ndEdition.Expand || c is Cards.Prosperity2ndEdition.Forge || c is Cards.Cornucopia.Remake || c is Cards.Cornucopia2ndEdition.Remake || c is Cards.Hinterlands.Develop || c is Cards.Hinterlands2ndEdition.Develop || c is Cards.DarkAges.Forager || c is Cards.DarkAges.Procession || c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Forager || c is Cards.DarkAges2ndEdition.Rats || c is Cards.DarkAges2019Errata.Procession || c is Cards.Guilds.Butcher || c is Cards.Guilds.Stonemason || c is Cards.Guilds2ndEdition.Butcher || c is Cards.Guilds2ndEdition.Stonemason || c is Cards.Empires.Sacrifice || c is Cards.Nocturne.ZombieMason || c is Cards.Renaissance.Hideout || c is Cards.Renaissance.Priest || c is Cards.Renaissance.Recruiter || c is Cards.Renaissance.Research || c is Cards.Menagerie.Scrap || c is Cards.Promotional.Dismantle ).Take(1)); // Only take at most 1 Fortress, as we only really care about exchanging Bat for Vampire at this point if (cardsToTrash.Count < 1) cardsToTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Fortress).Take(1)); return new ChoiceResult(new CardCollection(cardsToTrash)); } protected override ChoiceResult DecideBishop(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text.StartsWith("Trash a card.", StringComparison.InvariantCulture)) { // 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 var bishopBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Curse)); // Useless Seahags & Familiars go next if (bishopBestCard == null && _Game.Table.Curse.Count < _Game.Players.Count) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar); // Rats are usually a great choice as well if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Rats); // Ruins go next if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Ruins)); // Estates are usually a great choice as well if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Estate); // Overgrown Estates are a good choice as well if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate); // Fortress is sweet -- it comes right back into my hand if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Fortress); // Hovels aren't horrible to trash if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Hovel); // 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 is Cards.Base.Bureaucrat && GameProgressLeft < 0.4) || (c is Cards.Base.Moneylender && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || c is Cards.Base.Remodel || (c is Cards.Base2ndEdition.Bureaucrat && GameProgressLeft < 0.4) || (c is Cards.Base2ndEdition.Moneylender && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || (c is Cards.Intrigue.Coppersmith && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 5) || (c is Cards.Intrigue.Ironworks && GameProgressLeft < 0.4) || c is Cards.Intrigue.Masquerade || c is Cards.Intrigue.TradingPost || c is Cards.Intrigue.Upgrade || (c is Cards.Intrigue2ndEdition.Ironworks && GameProgressLeft < 0.4) || c is Cards.Intrigue2ndEdition.Masquerade || c is Cards.Seaside.Ambassador || c is Cards.Seaside.Lookout || c is Cards.Seaside.Salvager || (c is Cards.Seaside.TreasureMap && RealThis.CountAll(RealThis, cG => cG is Cards.Universal.Gold) > 2) || c is Cards.Seaside2ndEdition.Ambassador || c is Cards.Seaside2ndEdition.Lookout || c is Cards.Seaside2ndEdition.Salvager || (c is Cards.Seaside2ndEdition.TreasureMap && RealThis.CountAll(RealThis, cG => cG is Cards.Universal.Gold) > 2) || (c is Cards.Alchemy.Potion && !_Game.Table.TableEntities.Values.OfType().Any(s => s.BaseCost.Potion.Value > 0 && s.CanGain())) || (c is Cards.Prosperity.CountingHouse && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 5) || c is Cards.Prosperity.Expand || c is Cards.Prosperity.Forge || (c is Cards.Prosperity.Loan && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || (c is Cards.Prosperity2ndEdition.CountingHouse && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 5) || c is Cards.Prosperity2ndEdition.Expand || c is Cards.Prosperity2ndEdition.Forge || (c is Cards.Prosperity2ndEdition.Loan && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || c is Cards.Cornucopia.Remake || c is Cards.Cornucopia2ndEdition.Remake || c is Cards.Hinterlands.Develop || (c is Cards.Hinterlands.SpiceMerchant && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper || cC is Cards.Prosperity.Loan || cC is Cards.Prosperity2ndEdition.Loan) < 4) || c is Cards.Hinterlands2ndEdition.Develop || (c is Cards.Hinterlands2ndEdition.SpiceMerchant && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper || cC is Cards.Prosperity.Loan || cC is Cards.Prosperity2ndEdition.Loan) < 4) || (c is Cards.DarkAges.HuntingGrounds && GameProgressLeft < 0.4) || (c is Cards.DarkAges.SirVander && GameProgressLeft < 0.3) || (c is Cards.DarkAges.Armory && GameProgressLeft < 0.4) || (c is Cards.DarkAges2ndEdition.SirVander && GameProgressLeft < 0.3) || (c is Cards.DarkAges2ndEdition.Armory && GameProgressLeft < 0.4) || c is Cards.Guilds.Stonemason || c is Cards.Guilds2ndEdition.Stonemason ); // Copper is a distant 10th if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); // Masterpiece's main benefit is its on-buy ability, so might as well trash it now // Same goes for Ill-Gotten Gain's on-gain ability if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece || c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains); // 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 is Cards.Prosperity.Peddler); // Otherwise, choose a non-Victory card to trash if (bishopBestCard == null) { var bishCards = FindBestCardsToTrash(choice.Cards.Where(c => !c.Category.HasFlag(Categories.Victory)), 1).ToList(); if (bishCards.Any()) bishopBestCard = bishCards.ElementAt(0); } // Duchies or Dukes are usually an OK choice if (bishopBestCard == null) bishopBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Duchy || c is Cards.Intrigue.Duke ); // OK, last chance... just PICK one! if (bishopBestCard == null) { bishopBestCard = FindBestCardsToTrash(choice.Cards, 1).FirstOrDefault(); } if (bishopBestCard != null) return new ChoiceResult(new CardCollection { bishopBestCard }); return new ChoiceResult(new CardCollection(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.Any(c => c is Cards.Universal.Curse)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Curse) }); // Always choose to trash a Ruins from Bishop if I have one if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c.Category.HasFlag(Categories.Ruins)) }); // Also trash Overgrown Estates if (choice.Cards.Any(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate) }); // And Hovels, too... why not if (choice.Cards.Any(c => c is Cards.DarkAges.Hovel)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.DarkAges.Hovel) }); return new ChoiceResult(new CardCollection()); } } protected override ChoiceResult DecideBlessedVillage(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Triggers.Any(c => (c is Cards.Nocturne.TheFlamesGift && RealThis.Hand.Any(hC => hC is Cards.Universal.Curse || hC is Cards.Base.Remodel || hC is Cards.Base2ndEdition.Remodel || hC is Cards.Intrigue.TradingPost || hC is Cards.Intrigue.Upgrade || hC is Cards.Intrigue2ndEdition.TradingPost || hC is Cards.Seaside.Ambassador || hC is Cards.Seaside.Salvager || (hC is Cards.Seaside.SeaHag && _Game.Table.Curse.Count < _Game.Players.Count) || hC is Cards.Seaside2ndEdition.Ambassador || hC is Cards.Seaside2ndEdition.Salvager || (hC is Cards.Seaside2ndEdition.SeaHag && _Game.Table.Curse.Count < _Game.Players.Count) || hC is Cards.Prosperity.Expand || hC is Cards.Prosperity.Forge || hC is Cards.Prosperity.TradeRoute || hC is Cards.Prosperity2ndEdition.Expand || hC is Cards.Prosperity2ndEdition.Forge || hC is Cards.Prosperity2ndEdition.TradeRoute || (hC is Cards.Alchemy.Familiar && _Game.Table.Curse.Count < _Game.Players.Count) || hC is Cards.Alchemy.Transmute || hC is Cards.Cornucopia.Remake || hC is Cards.Cornucopia2ndEdition.Remake || hC is Cards.Hinterlands.Develop || (hC is Cards.Hinterlands.SpiceMerchant && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper || cC is Cards.Prosperity.Loan || cC is Cards.Prosperity2ndEdition.Loan) < 4) || hC is Cards.Hinterlands2ndEdition.Develop || (hC is Cards.Hinterlands2ndEdition.SpiceMerchant && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper || cC is Cards.Prosperity.Loan || cC is Cards.Prosperity2ndEdition.Loan) < 4) || (hC is Cards.DarkAges.Armory && GameProgressLeft < 0.4) || (hC is Cards.DarkAges2ndEdition.Armory && GameProgressLeft < 0.4) )) || c is Cards.Nocturne.TheMoonsGift && RealThis.DiscardPile.LookThrough(hC => hC.Category.HasFlag(Categories.Action) || hC.Category.HasFlag(Categories.Treasure) || hC.Category.HasFlag(Categories.Night)).Select(hC => ComputeValueInDeck(hC)).Any(v => v >= 5.0) || c is Cards.Nocturne.TheMountainsGift || c is Cards.Nocturne.TheRiversGift || c is Cards.Nocturne.TheSeasGift && RealThis.Hand.Count >= 3 || c is Cards.Nocturne.TheSunsGift || c is Cards.Nocturne.TheSwampsGift )) return new ChoiceResult(new List { choice.Options[0].Text }); // Receive next turn return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideBonfire(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 2, true))); } protected override ChoiceResult DecideBorderGuard(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: return new ChoiceResult(new ItemCollection { FindBestCardToPlay(choice.Cards) }); case ChoiceType.Options: // Prioritize Horn over Lantern in a vacuum var option = choice.Options[1]; // If we've already got the Horn, take the Lantern if (RealThis.Takeables.Any(t => t is Cards.Renaissance.Horn)) option = choice.Options[0]; return new ChoiceResult(new OptionCollection { option }); } return base.DecideBorderGuard(choice); } protected override ChoiceResult DecideBorderVillage(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideBountyHunter(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Short-term logic is to first always Exile cards we want that we don't have in Exile yet // TODO -- The big logic choice here is: do we want $3 or do we want a card // in our Exile that we want in our deck at some point later in the game var exileMat = RealThis.PlayerMats.ContainsKey(Cards.Menagerie.TypeClass.Exile) ? RealThis.PlayerMats[Cards.Menagerie.TypeClass.Exile] : new CardMat(); var nonExiledCards = choice.Cards.Where(c => exileMat.Any(ec => ec.Name == c.Name)); // Prioritize cards we don't want to play var dontWant = nonExiledCards.Where(c => !ShouldPlay(c)); if (dontWant.Any()) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(dontWant, 1))); // Get $3 rather than ditching a potentially-useful card for the future if (nonExiledCards.Any()) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(nonExiledCards, 1))); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); //var plusCoinBuyables = RealThis._Game.Table.TableEntities.Values.OfType().Where(s => // s.CanBuy(RealThis, RealThis.Currency) // && ShouldBuy(s) // ); //// Yay recursion! //var plusCoinScores = ValuateCardsToBuy(plusCoinBuyables); //var bestScore = plusCoinScores.Keys.OrderByDescending(k => k).FirstOrDefault(); //var previousBestScore = bestScore; //// Now see what our best buy is if we borrow a coin //plusCoinBuyables = RealThis._Game.Table.TableEntities.Values.OfType().Where(s => // !(s is Cards.Adventures.Borrow) // && !(s is Cards.Adventures2ndEdition.Borrow) // && s.CanBuy(RealThis, RealThis.Currency + new Currencies.Coin(3)) // && ShouldBuy(s) // ); //plusCoinScores = ValuateCardsToBuy(plusCoinBuyables); //bestScore = plusCoinScores.Keys.OrderByDescending(k => k).FirstOrDefault(); } protected override ChoiceResult DecideBureaucrat(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection { choice.Cards.ElementAt(_Game.RNG.Next(choice.Cards.Count())) }); } protected override ChoiceResult DecideBustlingVillage(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always "Yes" return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideButcher(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var coinTokens = RealThis.TokenPiles[Cards.Guilds.TypeClass.Coffer].Count; var spendCoinTokens = 0; var previousBestScore = -1.0; var trashedCardCost = RealThis._Game.ComputeCost(RealThis.CurrentTurn.CardsTrashed[RealThis.CurrentTurn.CardsTrashed.Count - 1]); for (var coinTokenCount = 0; coinTokenCount <= coinTokens; coinTokenCount++) { var scores = new Dictionary>(); foreach (var supply in RealThis._Game.Table.TableEntities.Values.OfType()) { if (supply.CanGain() && supply.CurrentCost <= trashedCardCost + new Currencies.Coin(coinTokenCount) && ShouldBuy(supply)) { var score = ComputeValueInDeck(supply.TopCard); if (!scores.ContainsKey(score)) scores[score] = new List(); scores[score].Add(supply); } } var bestScore = scores.Keys.OrderByDescending(k => k).FirstOrDefault(); if (bestScore > 0 && (previousBestScore < 0 || bestScore >= coinTokenCount - spendCoinTokens + 1 + previousBestScore)) { spendCoinTokens = coinTokenCount; previousBestScore = bestScore; } } return new ChoiceResult(new List { choice.Options.First(o => o.Text == spendCoinTokens.ToString(CultureInfo.InvariantCulture)).Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(choice.Supplies.Values.OfType().OrderByDescending(s => ComputeValueInDeck(s.TopCard)).First()); default: return base.DecideButcher(choice); } } protected override ChoiceResult DecideCamelTrain(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideCapital(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always pay off as many Debts as possible return new ChoiceResult(new List { choice.Options.Last().Text }); } protected override ChoiceResult DecideCardinal(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Choose Victory-only cards first Card bestCard = null; if (bestCard == null) bestCard = choice.Cards.Where(c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && !c.Category.HasFlag(Categories.Night) ).OrderByDescending(c => ComputeValueInDeck(c)) .FirstOrDefault(); // Next, any card we don't want in our deck anyway if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => !ShouldPlay(c)); // The cheapest/worst card in our deck finally if (bestCard == null) bestCard = choice.Cards.OrderBy(c => ComputeValueInDeck(c)).FirstOrDefault(); if (bestCard != null) return new ChoiceResult(bestCard); // Decide at random return base.DecideCardinal(choice); } protected override ChoiceResult DecideCartographer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == Resource.ChooseDiscards) { // Grab all cards that we don't really care about return new ChoiceResult(new CardCollection(choice.Cards.Where(IsCardOkForMeToDiscard))); } else { var cartCards = new CardCollection(choice.Cards); // Order them in roughly random order cartCards.Shuffle(); return new ChoiceResult(cartCards); } } protected override ChoiceResult DecideCatacombs(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // This is only OK -- it should scale the "discardability" of Action cards & Treasure cards according to how many Actions // I have available to play. 0 Actions should ramp up Action cards and ramp down Treasure cards. Multiple Actions should // do the inverse, though not quite as extremely var totalDeckDiscardability = RealThis.SumAll(RealThis, c => true, c => c is Card ? ComputeDiscardValue((Card)c) : 0, onlyCurrentlyDrawable: true); var totalCards = RealThis.CountAll(RealThis, c => true, onlyCurrentlyDrawable: true); var cardsDiscardability = choice.Triggers.OfType().Sum(c => ComputeDiscardValue(c)); // If it's better to keep these cards than discard them if (cardsDiscardability / choice.Triggers.Count >= totalDeckDiscardability / totalCards) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideCatacombs(choice); } } protected override ChoiceResult DecideCatapult(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == Resource.ChooseACardToTrash) { Card cardToTrash; // Different priority of cards to trash vs. the FindBestCardsToTrash method cardToTrash = choice.Cards.FirstOrDefault(c => c is Cards.Empires.Rocks); if (cardToTrash == null) cardToTrash = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); if (cardToTrash == null) cardToTrash = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains); if (cardToTrash == null) cardToTrash = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Talisman || c is Cards.Prosperity2ndEdition.Talisman); if (cardToTrash == null && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 3) cardToTrash = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan); if (cardToTrash == null && _Game.Table.Curse.Count == 0) cardToTrash = choice.Cards.FirstOrDefault(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag); if (cardToTrash == null) cardToTrash = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Rats); if (cardToTrash == null) cardToTrash = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Fortress); if (cardToTrash == null) cardToTrash = FindBestCardsToTrash(choice.Cards, 1).First(); return new ChoiceResult(new CardCollection { cardToTrash }); } if (choice.Text.StartsWith("Choose cards to discard.", StringComparison.InvariantCulture)) { return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } return base.DecideCatapult(choice); } protected override ChoiceResult DecideCathedral(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideCellar(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cellarCards = new CardCollection(); // TODO -- the AI makes slightly bad decisions when it comes to Cellar -- Fix me! cellarCards.AddRange(choice.Cards.Where(c => c is Cards.Base.Cellar)); cellarCards.AddRange(choice.Cards.Where(c => c is Cards.Universal.Curse)); cellarCards.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins))); cellarCards.AddRange(choice.Cards.Where(c => c is Cards.Hinterlands.Tunnel || c is Cards.Hinterlands2ndEdition.Tunnel)); cellarCards.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Hovel)); cellarCards.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate)); cellarCards.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Rats)); cellarCards.AddRange(choice.Cards.Where(c => (c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && c.Type != Cards.Universal.TypeClass.Estate && c.Type != Cards.Universal.TypeClass.Province))); if (choice.Cards.Any(c => c is Cards.Universal.Estate)) cellarCards.AddRange(choice.Cards.Where(c => c is Cards.Universal.Estate).Take(choice.Cards.Count(c => c is Cards.Universal.Estate) - choice.Cards.Count(c => c is Cards.Intrigue.Baron))); if (choice.Cards.Any(c => c is Cards.Universal.Province)) cellarCards.AddRange(choice.Cards.Where(c => c is Cards.Universal.Province).Take(choice.Cards.Count(c => c is Cards.Universal.Province) - choice.Cards.Count(c => c is Cards.Cornucopia.Tournament || c is Cards.Cornucopia2ndEdition.Tournament))); if (GameProgressLeft < 0.63) cellarCards.AddRange(choice.Cards.Where(c => c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece)); return new ChoiceResult(cellarCards); } protected override ChoiceResult DecideCemetery(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cemeteryToTrash = new CardCollection(); // Always choose to trash all Curses cemeteryToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Curse).Take(4)); // Always choose to trash all Ruins if (cemeteryToTrash.Count < 4) cemeteryToTrash.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins)).Take(4 - cemeteryToTrash.Count)); // Always choose to trash all Hovels if (cemeteryToTrash.Count < 4) cemeteryToTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Hovel).Take(4 - cemeteryToTrash.Count)); return new ChoiceResult(cemeteryToTrash); } protected override ChoiceResult DecideChancellor(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Never put deck into discard pile later in the game -- we don't want those Victory cards back in our deck sooner if (GameProgressLeft < 0.30) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideChangeling(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideChapel(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var chapelToTrash = new CardCollection(); // Always choose to trash all Curses chapelToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Curse).Take(4)); // Always choose to trash all Ruins if (chapelToTrash.Count < 4) chapelToTrash.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins)).Take(4 - chapelToTrash.Count)); // Always choose to trash all Hovels if (chapelToTrash.Count < 4) chapelToTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Hovel).Take(4 - chapelToTrash.Count)); return new ChoiceResult(chapelToTrash); } protected override ChoiceResult DecideCharm(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Lame & boring, but always choose +1 Buy / +2 Coin return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideCityGate(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideCobbler(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideConclave(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cardsToPlay = new CardCollection(); var bestCard = FindBestCardToPlay(choice.Cards.Where(c => !(c is Cards.Base.Chapel) && !(c is Cards.Base.Library) && !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.SecretChamber) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Outpost) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.Tactician) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Outpost) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.Tactician) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.CountingHouse) && !(c is Cards.Prosperity.Forge) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity.Watchtower) && !(c is Cards.Prosperity2ndEdition.CountingHouse) && !(c is Cards.Prosperity2ndEdition.Forge) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Prosperity2ndEdition.Watchtower) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.JunkDealer) && !(c is Cards.DarkAges.Procession) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.DarkAges2019Errata.Procession) && !(c is Cards.Guilds.MerchantGuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.MerchantGuild) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Adventures.DistantLands) && !(c is Cards.Adventures.Duplicate) && !(c is Cards.Adventures.Champion) && !(c is Cards.Adventures.Teacher) && !(c is Cards.Adventures.RoyalCarriage) && !(c is Cards.Adventures2ndEdition.Champion) && !(c is Cards.Adventures2ndEdition.RoyalCarriage) && !(c is Cards.Empires.Sacrifice) && !(c is Cards.Renaissance.Hideout) && !(c is Cards.Renaissance.Priest) && !(c is Cards.Renaissance.Recruiter) && !(c is Cards.Renaissance.Research) && !(c is Cards.Menagerie.Scrap) )); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(c => !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Empires.Sacrifice) && !(c is Cards.Renaissance.Hideout) && !(c is Cards.Renaissance.Priest) && !(c is Cards.Renaissance.Recruiter) && !(c is Cards.Renaissance.Research) && !(c is Cards.Menagerie.Scrap) )); if (bestCard != null) cardsToPlay.Add(bestCard); else if (choice.Minimum > 0) cardsToPlay.Add(choice.Cards.ElementAt(_Game.RNG.Next(choice.Cards.Count()))); return new ChoiceResult(cardsToPlay); } protected override ChoiceResult DecideCount(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var option = string.Empty; // First choice if (choice.Options[0].Text == Resource.Discard2Cards) { // 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 (RealThis.Hand.Count(c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) || c.Category.HasFlag(Categories.Ruins) || (c.Category.HasFlag(Categories.Action) && ActionsAvailable() <= 0) || ((c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag) && RealThis._Game.Table.Curse.Count < RealThis._Game.Players.Count) || ((c is Cards.Prosperity.CountingHouse || c is Cards.Prosperity.CountingHouse) && !RealThis.DiscardPile.Any(dc => dc is Cards.Universal.Copper)) || (c is Cards.Base.Moneylender && !RealThis.Hand.Any(hc => hc is Cards.Universal.Copper)) || (c is Cards.Base2ndEdition.Moneylender && !RealThis.Hand.Any(hc => hc is Cards.Universal.Copper)) || (( c is Cards.Base.ThroneRoom || c is Cards.Base2ndEdition.ThroneRoom || c is Cards.Prosperity.KingsCourt || c is Cards.Prosperity2ndEdition.KingsCourt || c is Cards.DarkAges.Procession || c is Cards.DarkAges2019Errata.Procession || c is Cards.Adventures.RoyalCarriage || c is Cards.Adventures2ndEdition.RoyalCarriage || c is Cards.Nocturne.Ghost ) && !RealThis.Hand.Any(hc => hc.Category.HasFlag(Categories.Action) && !(hc is Cards.Base.ThroneRoom) && !(hc is Cards.Base2ndEdition.ThroneRoom) && !(hc is Cards.Prosperity.KingsCourt) && !(hc is Cards.Prosperity2ndEdition.KingsCourt) && !(hc is Cards.DarkAges.Procession) && !(hc is Cards.DarkAges2019Errata.Procession) && !(hc is Cards.Adventures.RoyalCarriage) && !(hc is Cards.Adventures2ndEdition.RoyalCarriage) && !(hc is Cards.Nocturne.Ghost) )) || ((c is Cards.Seaside.TreasureMap || c is Cards.Seaside2ndEdition.TreasureMap) && RealThis.Hand.Count(hc => hc is Cards.Seaside.TreasureMap || hc is Cards.Seaside2ndEdition.TreasureMap) < 2 ) ) > 2) option = choice.Options[0].Text; else option = choice.Options[1].Text; } // Second choice else { if (GameProgressLeft <= 0.4 && RealThis._Game.Table.Duchy.CanGain()) option = choice.Options[2].Text; else if (( // Trash our hand if we have more than 3 of only Curse/Copper/Ruins/Rats cards RealThis.Hand.Count == RealThis.Hand[c => c is Cards.Universal.Copper || c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins) || c is Cards.DarkAges.Rats].Count) && RealThis.Hand.Count > 3 ) option = choice.Options[1].Text; else option = choice.Options[0].Text; } if (!string.IsNullOrEmpty(option)) return new ChoiceResult(new List { option }); break; case ChoiceType.Cards: if (choice.Text == Resource.Discard2Cards) { return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } if (choice.Text == Resource.TopdeckCard) { return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } break; } return base.DecideCount(choice); } protected override ChoiceResult DecideCounterfeit(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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 // Spoils are first, since they're super, super awesome w/ Counterfeit var counterfeitingCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Spoils); // If we've got a few Coppers left, let's do one of those // Masterpiece is basically in the same boat as Copper, so include that as well if (counterfeitingCard == null && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece) > 4) counterfeitingCard = choice.Cards.FirstOrDefault(card => card is Cards.Universal.Copper || card is Cards.Guilds.Masterpiece || card is Cards.Guilds2ndEdition.Masterpiece); // If we've got a Loan and not many Coppers left, we don't want to keep the Loan around anyway if (counterfeitingCard == null && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 4) counterfeitingCard = choice.Cards.FirstOrDefault(card => card is Cards.Prosperity.Loan || card is Cards.Prosperity2ndEdition.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 && GameProgressLeft < 0.50 && RealThis.Buys > 2) counterfeitingCard = choice.Cards.FirstOrDefault(card => card is Cards.Prosperity.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 && GameProgressLeft < 0.66 && (RealThis.CountAll(RealThis, c => c is Cards.Universal.Silver) >= 6 || RealThis.CountAll(RealThis, c => c is Cards.Universal.Gold || c is Cards.Intrigue.Harem || c is Cards.Alchemy.PhilosophersStone || c is Cards.Alchemy2ndEdition.PhilosophersStone || c is Cards.Prosperity.Bank || c is Cards.Prosperity.Platinum || c is Cards.Prosperity.RoyalSeal || c is Cards.Prosperity2ndEdition.RoyalSeal || c is Cards.Prosperity.Venture || c is Cards.Hinterlands.Cache || c is Cards.Hinterlands2ndEdition.Cache) >= 6)) counterfeitingCard = choice.Cards.FirstOrDefault(card => card is Cards.Universal.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 is Cards.Hinterlands.IllGottenGains || card is Cards.Hinterlands2ndEdition.IllGottenGains); if (counterfeitingCard != null) return new ChoiceResult(new CardCollection { counterfeitingCard }); // Don't play anything return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideCountingHouse(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always grab all of the coppers return new ChoiceResult(new List { choice.Options[choice.Options.Count - 1].Text }); } protected override ChoiceResult DecideContraband(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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) var multiplier = 2.0; if (GameProgressLeft > 0.85) multiplier = 1.75; else if (GameProgressLeft > 0.65) multiplier = 1.85; var remainingCoins = Math.Max(0, (int)(0.5 + multiplier * choice.PlayerSource.Hand.Count + 3 * Gaussian.NextGaussian(_Game.RNG))); var coinCost = choice.PlayerSource.Currency + new Currencies.Coin(remainingCoins); ISupply supply = null; while (supply == null) { supply = FindBestCardForCost( choice.Supplies.Values.OfType().Where( s => s.Any() && !s.Tokens.Any( t => t is Cards.Prosperity.ContrabandMarker && ((Cards.Prosperity.ContrabandMarker)t).Unbuyable.Name == s.TopCard.Name ) ), choice.PlayerSource.Currency + new Currencies.Coin(remainingCoins), false); remainingCoins++; } return new ChoiceResult(supply); } protected override ChoiceResult DecideCourtier(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var options = new List(); // Priorities for options are: // +1 Action if we have any stranded Action cards in hand // gain Gold if we can // +3 coin // +1 buy // +1 Action // gain Gold if (ActionsAvailable() == 0 && RealThis.Hand.Any(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c))) options.Add(choice.Options[0].Text); if (_Game.Table.Gold.CanGain()) options.Add(choice.Options[3].Text); options.Add(choice.Options[2].Text); options.Add(choice.Options[1].Text); options.Add(choice.Options[0].Text); options.Add(choice.Options[3].Text); return new ChoiceResult(options.Take(choice.Minimum).ToList()); case ChoiceType.Cards: // Find the card with the most types uint bestCount = 0; Card bestCard = null; foreach (var card in choice.Cards) { var count = (card.Category & ~Categories.Card).Count(); if (bestCount < count || FindBestCardsToDiscard(new List { bestCard, card }, 1).FirstOrDefault() == card) { bestCount = count; bestCard = card; } } if (bestCard != null) return new ChoiceResult(new CardCollection { bestCard }); break; } return base.DecideCourtier(choice); } protected override ChoiceResult DecideCourtyard(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideCropRotation(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Maybe there are other situational cards we'd want to keep around (e.g. Province w/ Tournament) -- need to investigate more var toDiscard = choice.Cards.FirstOrDefault(c => !( c is Cards.Intrigue.Harem || c is Cards.Intrigue.Nobles || c is Cards.Intrigue2ndEdition.Mill || c is Cards.Intrigue2ndEdition.Nobles || c is Cards.Seaside.Island || c is Cards.Seaside2ndEdition.Island || c is Cards.DarkAges.DameJosephine || c is Cards.DarkAges2ndEdition.DameJosephine || c is Cards.Adventures.DistantLands )); return new ChoiceResult(new CardCollection { toDiscard }); } protected override ChoiceResult DecideCrown(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierExclusionPredicate())); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierDestructiveExclusionPredicate())); if (bestCard != null) return new ChoiceResult(new CardCollection { bestCard }); // Don't play anything return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideCrypt(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == "Set aside any number of Treasures") { // Put all Treasures under the Crypt. There's lots of complicated decisions to be made about how many good-value cards // should go under it, but for now that number is "all" return new ChoiceResult(new CardCollection(choice.Cards)); } else { var bestCard = choice.Cards.OrderByDescending(card => ComputeValueInDeck(card)).First(); return new ChoiceResult(new CardCollection { bestCard }); } } protected override ChoiceResult DecideCultist(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always return "Yes" return new ChoiceResult(new List { choice.Options[0].Text }); // Yes } protected override ChoiceResult DecideDameAnna(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == "Choose up to 2 cards to trash") { var dameAnnaToTrash = new CardCollection(); // Always choose to trash all Curses dameAnnaToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Curse).Take(2)); // Always choose to trash all Ruins dameAnnaToTrash.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins)).Take(2 - dameAnnaToTrash.Count)); return new ChoiceResult(dameAnnaToTrash); } if (choice.Text == Resource.ChooseACardToTrash) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); return base.DecideDameAnna(choice); } protected override ChoiceResult DecideDameJosephine(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideDameMolly(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideDameNatalie(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text.StartsWith("You may gain a card", StringComparison.InvariantCulture)) { var supply = FindBestCardForCost(choice.Supplies.Values.OfType(), null, false); if (supply.Type == Cards.Universal.TypeClass.Curse || supply.Type == Cards.Universal.TypeClass.Copper || supply.Category.HasFlag(Categories.Ruins)) supply = null; return new ChoiceResult(supply); } if (choice.Text == Resource.ChooseACardToTrash) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); return base.DecideDameNatalie(choice); } protected override ChoiceResult DecideDameSylvia(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideDeathCart(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // This is pretty terrible logic overall. It should maybe be something a bit more flexible. // There are a few cards we'd rather keep around other than Death Cart var bestTrashCard = FindBestCardsToTrash(choice.Cards, 1).First(); if (bestTrashCard is Cards.Base.CouncilRoom || bestTrashCard is Cards.Base.Festival || bestTrashCard is Cards.Base.Laboratory || bestTrashCard is Cards.Intrigue.Minion || bestTrashCard is Cards.Intrigue.Nobles || bestTrashCard is Cards.Intrigue.Torturer || bestTrashCard is Cards.Seaside.Bazaar || bestTrashCard is Cards.Seaside.GhostShip || bestTrashCard is Cards.Seaside.MerchantShip || bestTrashCard is Cards.Seaside.Treasury || bestTrashCard is Cards.Seaside.Wharf || bestTrashCard is Cards.Seaside2ndEdition.GhostShip || bestTrashCard is Cards.Seaside2ndEdition.Treasury || bestTrashCard is Cards.Alchemy.Golem || bestTrashCard is Cards.Alchemy2ndEdition.Golem || bestTrashCard is Cards.Prosperity.City || bestTrashCard is Cards.Prosperity.Goons || bestTrashCard is Cards.Prosperity.GrandMarket || bestTrashCard is Cards.Prosperity.KingsCourt || bestTrashCard is Cards.Prosperity.Mountebank || bestTrashCard is Cards.Prosperity.Peddler || bestTrashCard is Cards.Prosperity.Rabble || bestTrashCard is Cards.Prosperity.Vault || bestTrashCard is Cards.Prosperity2ndEdition.City || bestTrashCard is Cards.Prosperity2ndEdition.Goons || bestTrashCard is Cards.Prosperity2ndEdition.GrandMarket || bestTrashCard is Cards.Prosperity2ndEdition.KingsCourt || bestTrashCard is Cards.Prosperity2ndEdition.Mountebank || bestTrashCard is Cards.Prosperity2ndEdition.Rabble || bestTrashCard is Cards.Prosperity2ndEdition.Vault || bestTrashCard is Cards.Hinterlands.Embassy || bestTrashCard is Cards.Hinterlands.Highway || bestTrashCard is Cards.Hinterlands.Mandarin || bestTrashCard is Cards.Hinterlands.Margrave || bestTrashCard is Cards.Hinterlands.Stables || bestTrashCard is Cards.Hinterlands2ndEdition.Mandarin || bestTrashCard is Cards.Hinterlands2ndEdition.Stables || bestTrashCard is Cards.DarkAges.Altar || bestTrashCard is Cards.DarkAges.BanditCamp || bestTrashCard is Cards.DarkAges.Mystic || bestTrashCard is Cards.DarkAges.Pillage || bestTrashCard is Cards.DarkAges2ndEdition.Mystic || bestTrashCard is Cards.DarkAges2ndEdition.Pillage || bestTrashCard is Cards.DarkAges2019Errata.Pillage || bestTrashCard is Cards.Guilds.Baker || bestTrashCard is Cards.Guilds.Journeyman || bestTrashCard is Cards.Guilds.MerchantGuild || bestTrashCard is Cards.Guilds.Soothsayer || bestTrashCard is Cards.Guilds2ndEdition.Baker || bestTrashCard is Cards.Guilds2ndEdition.Journeyman || bestTrashCard is Cards.Guilds2ndEdition.MerchantGuild || bestTrashCard is Cards.Guilds2ndEdition.Soothsayer || bestTrashCard.Category.HasFlag(Categories.Prize) || bestTrashCard.Category.HasFlag(Categories.Knight) || (bestTrashCard is Cards.Base.Witch && RealThis._Game.Table.Curse.Count > RealThis._Game.Players.Count - 1) || (bestTrashCard is Cards.DarkAges.Cultist && ((ISupply)RealThis._Game.Table[Cards.DarkAges.TypeClass.RuinsSupply]).Count > RealThis._Game.Players.Count - 1) || (bestTrashCard is Cards.DarkAges.HuntingGrounds && GameProgressLeft > 0.5) || (bestTrashCard is Cards.DarkAges2ndEdition.Cultist && ((ISupply)RealThis._Game.Table[Cards.DarkAges.TypeClass.RuinsSupply]).Count > RealThis._Game.Players.Count - 1) || bestTrashCard is Cards.Adventures.BridgeTroll || bestTrashCard is Cards.Adventures.Champion || bestTrashCard is Cards.Adventures.Disciple || bestTrashCard is Cards.Adventures.DistantLands || bestTrashCard is Cards.Adventures.Fugitive || bestTrashCard is Cards.Adventures.Giant || bestTrashCard is Cards.Adventures.HauntedWoods || bestTrashCard is Cards.Adventures.Hero || bestTrashCard is Cards.Adventures.Hireling || bestTrashCard is Cards.Adventures.LostCity || bestTrashCard is Cards.Adventures.RoyalCarriage || bestTrashCard is Cards.Adventures.Storyteller || bestTrashCard is Cards.Adventures.SwampHag || bestTrashCard is Cards.Adventures.Teacher || bestTrashCard is Cards.Adventures.Warrior || bestTrashCard is Cards.Adventures.WineMerchant || bestTrashCard is Cards.Adventures2ndEdition.BridgeTroll || bestTrashCard is Cards.Adventures2ndEdition.Champion || bestTrashCard is Cards.Adventures2ndEdition.Giant || bestTrashCard is Cards.Adventures2ndEdition.HauntedWoods || bestTrashCard is Cards.Adventures2ndEdition.Hero || bestTrashCard is Cards.Adventures2ndEdition.RoyalCarriage || bestTrashCard is Cards.Adventures2ndEdition.Storyteller || bestTrashCard is Cards.Adventures2ndEdition.SwampHag || bestTrashCard is Cards.Adventures2ndEdition.Warrior || bestTrashCard is Cards.Empires.Archive || bestTrashCard is Cards.Empires.BustlingVillage || bestTrashCard is Cards.Empires.CityQuarter || bestTrashCard is Cards.Empires.Crown || bestTrashCard is Cards.Empires.Emporium || bestTrashCard is Cards.Empires.Forum || bestTrashCard is Cards.Empires.Groundskeeper || bestTrashCard is Cards.Empires.Legionary || bestTrashCard is Cards.Empires.OpulentCastle || bestTrashCard is Cards.Empires.Overlord || bestTrashCard is Cards.Empires.RoyalBlacksmith || bestTrashCard is Cards.Empires.WildHunt || bestTrashCard is Cards.Empires2019Errata.Overlord || bestTrashCard is Cards.Nocturne.CursedVillage || bestTrashCard is Cards.Nocturne.Pooka || bestTrashCard is Cards.Nocturne.SacredGrove || bestTrashCard is Cards.Nocturne.Tormentor || bestTrashCard is Cards.Nocturne.TragicHero || bestTrashCard is Cards.Nocturne.Werewolf || bestTrashCard is Cards.Renaissance.OldWitch || bestTrashCard is Cards.Renaissance.Recruiter || bestTrashCard is Cards.Renaissance.Scholar || bestTrashCard is Cards.Renaissance.Sculptor || bestTrashCard is Cards.Renaissance.Seer || bestTrashCard is Cards.Renaissance.Swashbuckler || bestTrashCard is Cards.Renaissance.Treasurer || bestTrashCard is Cards.Renaissance.Villain || bestTrashCard is Cards.Menagerie.AnimalFair || bestTrashCard is Cards.Menagerie.Barge || bestTrashCard is Cards.Menagerie.Coven || bestTrashCard is Cards.Menagerie.Destrier || bestTrashCard is Cards.Menagerie.Displace || bestTrashCard is Cards.Menagerie.Falconer || bestTrashCard is Cards.Menagerie.Fisherman || bestTrashCard is Cards.Menagerie.Gatekeeper || bestTrashCard is Cards.Menagerie.HuntingLodge || bestTrashCard is Cards.Menagerie.Kiln || bestTrashCard is Cards.Menagerie.Livery || bestTrashCard is Cards.Menagerie.Mastermind || bestTrashCard is Cards.Menagerie.Paddock || bestTrashCard is Cards.Menagerie.Sanctuary || bestTrashCard is Cards.Menagerie.Wayfarer || bestTrashCard is Cards.Promotional.Avanto || bestTrashCard is Cards.Promotional.Governor || bestTrashCard is Cards.Promotional.Prince ) return new ChoiceResult(new CardCollection()); return new ChoiceResult(new CardCollection { bestTrashCard }); } protected override ChoiceResult DecideDelay(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cards = new CardCollection(); var cardToPlay = FindBestCardToPlay(choice.Cards); if (cardToPlay != null) cards.Add(cardToPlay); return new ChoiceResult(cards); } protected override ChoiceResult DecideDemand(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideDesperation(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // The only reason to buy it is to use it. The real decision should be in the Buy logic return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideDevelop(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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.OfType(), null, false)); default: return base.DecideDevelop(choice); } } protected override ChoiceResult DecideDevilsWorkshop(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideDiplomat(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 3))); } protected override ChoiceResult DecideDisciple(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierExclusionPredicate())); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierDestructiveExclusionPredicate())); if (bestCard != null) return new ChoiceResult(new CardCollection { bestCard }); // Don't play anything return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideDisplace(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // Displace is a little fancier since we can Exile Victory cards without much penalty // (and generally prefer to Exile them anyway) var bestVictory = choice.Cards.Where(c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && !c.Category.HasFlag(Categories.Night) ).OrderByDescending(c => ComputeValueInDeck(c)).FirstOrDefault(); if (bestVictory != null) return new ChoiceResult(bestVictory); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideRemodel(choice); } } protected override ChoiceResult DecideDonate(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var donateToTrash = new CardCollection(); // Always choose to trash all Curses donateToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Curse)); // Always choose to trash all Ruins donateToTrash.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins))); // Always choose to trash all Hovels & Overgrown Estates (the on-trash benefit isn't worth the effort) donateToTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Hovel || c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate)); // If there are no Curses, trash Seahag and Familiar if (!_Game.Table.Curse.CanGain()) donateToTrash.AddRange(choice.Cards.Where(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar)); // If it's early, trash all Estates if (GameProgressLeft > 0.8) donateToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Estate)); // If it's midgame/late and we don't have any Copper combo cards, trash all Coppers if (GameProgressLeft < 0.6 && RealThis.CountAll( predicate: c => c is Cards.Base.Gardens || c is Cards.Intrigue.Coppersmith || c is Cards.Alchemy.Apothecary || c is Cards.Alchemy2ndEdition.Apothecary || c is Cards.Prosperity.CountingHouse || c is Cards.Prosperity.Bishop || c is Cards.Prosperity2ndEdition.CountingHouse || c is Cards.Prosperity2ndEdition.Bishop || c is Cards.Adventures.Miser) == 0 && !_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Fountain) && !_Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.Palace) && ComputeAverageCoinValueInDeck(c => c.Category.HasFlag(Categories.Treasure) && !(c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece)) > 1.5 ) donateToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece)); return new ChoiceResult(donateToTrash); } protected override ChoiceResult DecideDoctor(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: if (choice.Text.StartsWith("Do you want to discard", StringComparison.InvariantCulture)) { // 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 Curse, Ruins, Hovel, Overgrown Estate, or Rats, trash it // If it's a Copper/Masterpiece, 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.Triggers[0].Type == Cards.Universal.TypeClass.Curse || choice.Triggers[0].Type == Cards.DarkAges.TypeClass.Hovel || choice.Triggers[0].Type == Cards.DarkAges.TypeClass.OvergrownEstate || choice.Triggers[0].Type == Cards.DarkAges.TypeClass.Rats || choice.Triggers[0].Type == Cards.DarkAges2ndEdition.TypeClass.OvergrownEstate || choice.Triggers[0].Type == Cards.DarkAges2ndEdition.TypeClass.Rats || choice.Triggers[0].Type == Cards.Universal.TypeClass.Copper || choice.Triggers[0].Type == Cards.Guilds.TypeClass.Masterpiece || choice.Triggers[0].Type == Cards.Guilds2ndEdition.TypeClass.Masterpiece || choice.Triggers[0].Category.HasFlag(Categories.Ruins) || ((choice.Triggers[0].Type == Cards.Universal.TypeClass.Silver || choice.Triggers[0].Type == Cards.Prosperity.TypeClass.Talisman || choice.Triggers[0].Type == Cards.Prosperity.TypeClass.Quarry || choice.Triggers[0].Type == Cards.Prosperity2ndEdition.TypeClass.Talisman) && RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Platinum) > 0 && RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Venture) > 3) || ((choice.Triggers[0].Type == Cards.Prosperity.TypeClass.Loan || choice.Triggers[0].Type == Cards.Prosperity2ndEdition.TypeClass.Loan) && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 3)) return new ChoiceResult(new List { choice.Options[1].Text }); // Trash if ( (choice.Triggers[0].Category.HasFlag(Categories.Victory) && !choice.Triggers[0].Category.HasFlag(Categories.Action) && !choice.Triggers[0].Category.HasFlag(Categories.Treasure)) || choice.Triggers[0].Type == Cards.Universal.TypeClass.Copper ) return new ChoiceResult(new List { choice.Options[0].Text }); // Discard return new ChoiceResult(new List { choice.Options[2].Text }); // Put it back } for (var index = choice.Options.Count - 1; index >= 0; index--) { Currency overpayAmount = null; try { // Overpay by up to 4 overpayAmount = new Currency(choice.Options[index].Text); if (overpayAmount.Potion.Value > 0 || overpayAmount.Coin.Value > 4) continue; } finally { if (!(overpayAmount is null)) overpayAmount.Dispose(); } return new ChoiceResult(new List { choice.Options[index].Text }); } return base.DecideDoctor(choice); case ChoiceType.Cards: var doctorCards = new CardCollection(choice.Cards); // Order them in roughly random order doctorCards.Shuffle(); return new ChoiceResult(doctorCards); case ChoiceType.SuppliesAndCards: // This should choose cards we don't want. Obvious first targets are Curses, Ruins, Coppers, and perhaps Estates (early, early on) if (RealThis.CountAll(RealThis, c => c is Cards.Universal.Curse, false, true) > 0) return new ChoiceResult(choice.Supplies.Select(kvp => kvp.Value).OfType().FirstOrDefault(s => s.Type == Cards.Universal.TypeClass.Curse)); // Let's get rid of Rats as quickly as possible -- We, as the AI, *HATE* Rats if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.Rats, false, true) > 0) return new ChoiceResult(choice.Supplies.Select(kvp => kvp.Value).OfType().FirstOrDefault(s => s.Type == Cards.DarkAges.TypeClass.Rats)); // These priorities are based on nothing more than my intuition of how "good" each of the following Ruins cards are if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.RuinedVillage, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedVillage) }); if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.RuinedMarket, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedMarket) }); if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.Survivors, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Survivors) }); if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.Hovel, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Hovel) }); if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.AbandonedMine, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.AbandonedMine) }); if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.RuinedLibrary, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedLibrary) }); if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.OvergrownEstate, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.OvergrownEstate) }); if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges2ndEdition.OvergrownEstate, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges2ndEdition.OvergrownEstate) }); // Fortresses go into our hand when trashed, so yay! if (RealThis.CountAll(RealThis, c => c is Cards.DarkAges.Fortress, false, true) > 0) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Fortress) }); if (GameProgressLeft > 0.65 && RealThis.CountAll(RealThis, c => c is Cards.Universal.Estate, false, true) > 0) return new ChoiceResult(choice.Supplies.Select(kvp => kvp.Value).OfType().FirstOrDefault(s => s.Type == Cards.Universal.TypeClass.Estate)); if (GameProgressLeft < 0.65 && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper, false, true) > 0) return new ChoiceResult(choice.Supplies.Select(kvp => kvp.Value).OfType().FirstOrDefault(s => s.Type == Cards.Universal.TypeClass.Copper)); if (GameProgressLeft < 0.65 && RealThis.CountAll(RealThis, c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece, false, true) > 0) return new ChoiceResult(choice.Supplies.Select(kvp => kvp.Value).OfType().FirstOrDefault(s => s.Type == Cards.Guilds.TypeClass.Masterpiece || s.Type == Cards.Guilds2ndEdition.TypeClass.Masterpiece)); // We don't want to trash anything useful, so just choose Curse return new ChoiceResult(choice.Supplies.Select(kvp => kvp.Value).OfType().FirstOrDefault(s => s.Type == Cards.Universal.TypeClass.Curse)); default: return base.DecideDoctor(choice); } } protected override ChoiceResult DecideDruid(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Choose The Flame's Gift if we have any Curses or Ruins in our hand if (choice.Boons.Any(b => b is Cards.Nocturne.TheFlamesGift) && RealThis.Hand.Any(c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheFlamesGift) }); // Choose The Field's Gift if we need an Action to play a playable Action card in our hand if (choice.Boons.Any(b => b is Cards.Nocturne.TheFieldsGift) && ActionsAvailable() == 0 && RealThis.Hand.Any(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c))) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheFieldsGift) }); // High-value good cards are good to put on top if (choice.Boons.Any(b => b is Cards.Nocturne.TheMoonsGift) && RealThis.DiscardPile.LookThrough(hC => hC.Category.HasFlag(Categories.Action) || hC.Category.HasFlag(Categories.Treasure) || hC.Category.HasFlag(Categories.Night)) .Select(hC => ComputeValueInDeck(hC)).Any(v => v >= 6.0)) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheMoonsGift) }); // The Swamp's Gift is good if (choice.Boons.Any(b => b is Cards.Nocturne.TheSwampsGift)) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheSwampsGift) }); // High-value good cards are good to put on top if (choice.Boons.Any(b => b is Cards.Nocturne.TheMoonsGift) && RealThis.DiscardPile.LookThrough(hC => hC.Category.HasFlag(Categories.Action) || hC.Category.HasFlag(Categories.Treasure) || hC.Category.HasFlag(Categories.Night)) .Select(hC => ComputeValueInDeck(hC)).Any(v => v >= 5.0)) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheMoonsGift) }); // The River's Gift & The Sun's Gift are "ok". Choose one at random if (choice.Boons.Any(b => b is Cards.Nocturne.TheRiversGift || b is Cards.Nocturne.TheSunsGift)) return new ChoiceResult(new ItemCollection { choice.Boons.Where(b => b is Cards.Nocturne.TheRiversGift || b is Cards.Nocturne.TheSunsGift).ToList().Choose() }); // (not so) High-value good cards are still ok to put on top if (choice.Boons.Any(b => b is Cards.Nocturne.TheMoonsGift) && RealThis.DiscardPile.LookThrough(hC => hC.Category.HasFlag(Categories.Action) || hC.Category.HasFlag(Categories.Treasure) || hC.Category.HasFlag(Categories.Night)) .Select(hC => ComputeValueInDeck(hC)).Any(v => v >= 3.0)) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheMoonsGift) }); // The Forest's Gift, The Mountain's Gift, The Sea's Gift, The Sky's Gift, & The Wind's Gift are passable. Choose one at random if (choice.Boons.Any(b => b is Cards.Nocturne.TheForestsGift || b is Cards.Nocturne.TheMountainsGift || b is Cards.Nocturne.TheSeasGift || b is Cards.Nocturne.TheSkysGift || b is Cards.Nocturne.TheWindsGift)) return new ChoiceResult(new ItemCollection { choice.Boons.Where(b => b is Cards.Nocturne.TheForestsGift || b is Cards.Nocturne.TheMountainsGift || b is Cards.Nocturne.TheSeasGift || b is Cards.Nocturne.TheSkysGift || b is Cards.Nocturne.TheWindsGift).ToList().Choose() }); // The Earth's Gift is preferrable to The Flame's Gift if (choice.Boons.Any(b => b is Cards.Nocturne.TheEarthsGift)) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheEarthsGift) }); // Choose at random I guess return base.DecideDruid(choice); } protected override ChoiceResult DecideDucat(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always return "Yes" return new ChoiceResult(new List { choice.Options[0].Text }); // Yes } protected override ChoiceResult DecideDuchess(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (IsCardOkForMeToDiscard(choice.Triggers.OfType().First())) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideDungeon(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } protected override ChoiceResult DecideEmbargo(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var embargoAbleSupplies = new List(); foreach (var supply in choice.Supplies.Values.OfType().Where(s => s.TableableType != Cards.Universal.TypeClass.Curse)) { // Only allow at most 4 Embargo tokens on a Supply pile if (!ShouldBuy(supply) && supply.Tokens.Count(t => t is Cards.Seaside.EmbargoToken) < 4) embargoAbleSupplies.Add(supply); } if (embargoAbleSupplies.Count == 0) embargoAbleSupplies.Add((ISupply)choice.Supplies[Cards.Universal.TypeClass.Province]); return new ChoiceResult(embargoAbleSupplies[_Game.RNG.Next(embargoAbleSupplies.Count)]); } protected override ChoiceResult DecideEmbassy(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 3))); } protected override ChoiceResult DecideEncampment(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always reveal return new ChoiceResult(new CardCollection { choice.Cards.First() }); } protected override ChoiceResult DecideEngineer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); case ChoiceType.Options: // At some point, Engineer isn't useful any more as a card gainer, so let's just trash it if (GameProgressLeft < 0.35) return new ChoiceResult(new List { choice.Options[0].Text }); // Yes return new ChoiceResult(new List { choice.Options[1].Text }); // No } return base.DecideEngineer(choice); } protected override ChoiceResult DecideEnhance(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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.OfType(), null, false)); default: return base.DecideEnhance(choice); } } protected override ChoiceResult DecideEnvoy(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Find most-expensive non-Victory card to discard // Focus only on Treasure cards if there are no Actions remaining Card cardEnvoy = null; foreach (var card in choice.Cards) { if ((card.Category.HasFlag(Categories.Treasure) || (ActionsAvailable(_Game.TurnsTaken.Last().Player) > 0 && card.Category.HasFlag(Categories.Action))) && (cardEnvoy == null || ComputeValueInDeck(card) > ComputeValueInDeck(cardEnvoy))) cardEnvoy = card; } if (cardEnvoy != null) return new ChoiceResult(new CardCollection { cardEnvoy }); return base.DecideEnvoy(choice); } protected override ChoiceResult DecideExorcist(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceOutcome) { case ChoiceOutcome.Trash: var toTrash = new CardCollection(); // "On Gain" utility cards are great for Exorcist toTrash.AddRange(choice.Cards.Where(c => c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains).Take(1)); // Cultist with no Ruins is pretty good if (toTrash.Count == 0 && _Game.Table[Cards.DarkAges.TypeClass.RuinsSupply].Count == 0) toTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Cultist || c is Cards.DarkAges2ndEdition.Cultist).Take(1)); // "On Gain" utility cards are great for Exorcist if (toTrash.Count == 0) toTrash.AddRange(choice.Cards.Where(c => c is Cards.Guilds.Doctor || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Doctor || c is Cards.Guilds2ndEdition.Masterpiece).Take(1)); // Fortress works in a pinch if (toTrash.Count == 0) toTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Fortress).Take(1)); // Rocks works OK if (toTrash.Count == 0) toTrash.AddRange(choice.Cards.Where(c => c is Cards.Empires.Rocks).Take(1)); // Marauder with no Ruins loses some of its appeal, so is a decent option if (toTrash.Count == 0 && _Game.Table[Cards.DarkAges.TypeClass.RuinsSupply].Count == 0) toTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Marauder).Take(1)); // Hovel and Overgrown Estate get a Will-o'-Wisp if (toTrash.Count == 0) toTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Hovel || c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate).Take(1)); if (toTrash.Count == 0) toTrash.AddRange(FindBestCardsToTrash(choice.Cards, 1)); return new ChoiceResult(new CardCollection(toTrash.Take(1))); case ChoiceOutcome.Gain: return new ChoiceResult(new CardCollection { choice.Cards.OrderByDescending(c => c.BaseCost).First() }); default: return base.DecideExorcist(choice); } } protected override ChoiceResult DecideExpand(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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.OfType(), null, false)); default: return base.DecideExpand(choice); } } protected override ChoiceResult DecideExplorer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always reveal a Province if we can return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideFalconer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideFarmland(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // Always trash Curses if we can var farmlandBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Curse)); // Trashing a 9-cost non-Victory card for a Colony later in the game seems like a good idea if (farmlandBestCard == null && GameProgressLeft < 0.5 && _Game.Table.TableEntities.ContainsKey(Cards.Prosperity.TypeClass.Colony) && ((ISupply)_Game.Table.TableEntities[Cards.Prosperity.TypeClass.Colony]).CanGain()) farmlandBestCard = choice.Cards.FirstOrDefault(c => !c.Category.HasFlag(Categories.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 && GameProgressLeft < 0.5 && ((ISupply)_Game.Table[Cards.Universal.TypeClass.Province]).CanGain()) farmlandBestCard = choice.Cards.FirstOrDefault(c => !c.Category.HasFlag(Categories.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.TableEntities.ContainsKey(Cards.Prosperity.TypeClass.Colony) && ((ISupply)_Game.Table.TableEntities[Cards.Prosperity.TypeClass.Colony]).CanGain()) farmlandBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Victory) && c.BaseCost == new Cost(9)); // Trashing a 6-cost Victory card for a Province seems like a good idea if (farmlandBestCard == null && ((ISupply)_Game.Table[Cards.Universal.TypeClass.Province]).CanGain()) farmlandBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Victory) && c.BaseCost == new Cost(6)); // Trashing Masterpiece for a Duchy (or other 5-cost?) seems good if (farmlandBestCard == null) farmlandBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); // Trash Copper later in the game -- they just suck if (farmlandBestCard == null && GameProgressLeft < 0.65) farmlandBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); if (farmlandBestCard != null) return new ChoiceResult(new CardCollection { farmlandBestCard }); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideFarmland(choice); } } protected override ChoiceResult DecideFear(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideFeast(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideFerry(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType().Where(s => s.BaseCost.Coin >= 2), null, false)); } protected override ChoiceResult DecideFollowers(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult DecideFool(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // The order largely doesn't matter for these, but let's do the gainers first if (choice.Boons.Any(b => b is Cards.Nocturne.TheSwampsGift)) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheSwampsGift) }); if (choice.Boons.Any(b => b is Cards.Nocturne.TheMountainsGift)) return new ChoiceResult(new ItemCollection { choice.Boons.First(b => b is Cards.Nocturne.TheMountainsGift) }); return base.DecideFool(choice); } protected override ChoiceResult DecideForager(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (!RealThis._Game.Table.Trash.Any(c => c.Category.HasFlag(Categories.Treasure)) && choice.Cards.Any(c => c is Cards.Universal.Copper)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Copper) }); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideForge(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // Only trash Curses & Ruins var forgeToTrash = new CardCollection(); // Always choose to trash all Curses forgeToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Curse)); // Always choose to trash all Ruins forgeToTrash.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins))); return new ChoiceResult(forgeToTrash); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideForge(choice); } } protected override ChoiceResult DecideForum(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } protected override ChoiceResult DecideFugitive(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideGamble(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Triggers.OfType().Any() && ShouldPlay(choice.Triggers.OfType().First())) return new ChoiceResult(new List { choice.Options[0].Text }); // Yes return new ChoiceResult(new List { choice.Options[1].Text }); // No } protected override ChoiceResult DecideGear(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (ActionsAvailable() == 0) { var gearBestCards = choice.Cards.Where(c => c.Category.HasFlag(Categories.Action)).OrderByDescending(ComputeValueInDeck).Take(2).ToList(); if (gearBestCards.Count < 2 && RealThis.Currency.Coin < 3) { IEnumerable gearNonTreasures = choice.Cards.Where(c => !c.Category.HasFlag(Categories.Treasure)).OrderByDescending(ComputeValueInDeck); gearBestCards = gearBestCards.Union(gearNonTreasures.Take(2 - gearBestCards.Count)).ToList(); } return new ChoiceResult(new CardCollection(gearBestCards)); } if (RealThis.Currency.Coin > 8) { IEnumerable gearNonTreasures = choice.Cards.Where(c => !c.Category.HasFlag(Categories.Treasure)).OrderByDescending(ComputeValueInDeck); return new ChoiceResult(new CardCollection(gearNonTreasures.Take(2))); } if (RealThis.Currency.Coin > 6) { IEnumerable gearNonTreasures = choice.Cards.Where(c => !c.Category.HasFlag(Categories.Treasure)).OrderBy(ComputeValueInDeck); return new ChoiceResult(new CardCollection(gearNonTreasures.Take(1))); } // We don't have a lot of gold, so choose our biggest coin var gearTreasures = choice.Cards.Where(c => c.Category.HasFlag(Categories.Treasure)); // If there are no Treasures, try to grab Action cards instead if (!gearTreasures.Any()) { gearTreasures = choice.Cards.Where(c => c.Category.HasFlag(Categories.Action)); // Just pick any old card if (!gearTreasures.Any()) gearTreasures = choice.Cards; } return new ChoiceResult(new CardCollection(gearTreasures.Take(_Game.RNG.Next(0, 2)))); } protected override ChoiceResult DecideGhostShip(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult DecideGladiator(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: return new ChoiceResult(new List { choice.Options[0].Text }); // Yes case ChoiceType.Cards: var toLeft = _Game.GetPlayerFromIndex(this, 1); var unlikelyCards = choice.Cards; // Filter out cards we know the player to our left definitely has in hand if (KnownPlayerHands.ContainsKey(toLeft.UniqueId)) unlikelyCards = unlikelyCards.Where(card => KnownPlayerHands[toLeft.UniqueId].All(c => c.Type != card.Type)); // Sort by the likelihood that the player might have the card in their hand (based on the number of those cards they have in their deck) var unlikelyCard = unlikelyCards .OrderBy(card => toLeft.CountAll(this, c => c.Type == card.Type)) .FirstOrDefault(); if (unlikelyCard != null) return new ChoiceResult(new CardCollection { unlikelyCard }); // Otherwise, fall through and do whatever Basic would do break; } return base.DecideGladiator(choice); } protected override ChoiceResult DecideGoat(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1, true))); } protected override ChoiceResult DecideGoatherd(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1, true))); } protected override ChoiceResult DecideGolem(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Just choose one at random. return new ChoiceResult(new CardCollection { choice.Cards.ElementAt(_Game.RNG.Next(choice.Cards.Count())) }); } protected override ChoiceResult DecideGoons(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult DecideGovernor(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: if (RealThis.Hand[c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins)].Any()) return new ChoiceResult(new List { choice.Options[2].Text }); // Trash a card return new ChoiceResult(new List { choice.Options[0].Text }); // +1(+3) Cards case ChoiceType.Cards: // Only ever trash Curses or Ruins if (choice.Cards.Any(c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); return new ChoiceResult(new CardCollection()); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideGovernor(choice); } } protected override ChoiceResult DecideGraverobber(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var 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 (GameProgressLeft < 0.4 && RealThis._Game.Table.Trash.Any(c => availableCosts.Any(cost => cost == RealThis._Game.ComputeCost(c)) && c.Category.HasFlag(Categories.Victory))) return new ChoiceResult(new List { choice.Options[0].Text }); // Gain a card from the trash // Choose to trash a Ruins from Graverobber if I have one if (RealThis.Hand.Any(c => c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new List { choice.Options[1].Text }); // Trash an Action card return new ChoiceResult(new List { choice.Options[0].Text }); // Choose a card to gain from the trash case ChoiceType.Cards: if (choice.Text == "Choose a card to gain 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 (GameProgressLeft < 0.4 && choice.Cards.Any(c => c.Category.HasFlag(Categories.Victory))) return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards.Where(c => c.Category.HasFlag(Categories.Victory)), 1))); return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); } // "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.OfType(), null, false)); default: return base.DecideGraverobber(choice); } } protected override ChoiceResult DecideGroom(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var supplies = choice.Supplies.Values.OfType(); // Prioritize multi-type cards var bestMultiCard = FindBestCardForCost(supplies.Where(s => (s.Category.HasFlag(Categories.Action) && s.Category.HasFlag(Categories.Treasure)) || (s.Category.HasFlag(Categories.Treasure) && s.Category.HasFlag(Categories.Victory)) || (s.Category.HasFlag(Categories.Action) && s.Category.HasFlag(Categories.Victory)) ), null, false); return new ChoiceResult(bestMultiCard ?? FindBestCardForCost(supplies, null, false)); } protected override ChoiceResult DecideHaggler(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideHamlet(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == "You may discard a card for +1 Action.") { // Tunnel is always a great bet var hamletABestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.Tunnel || c is Cards.Hinterlands2ndEdition.Tunnel); // No? How about others... if (hamletABestCard == null) { var actionTerminationsLeft = RealThis.Hand.Count(c => ShouldPlay(c) && c.Category.HasFlag(Categories.Action) && !c.Traits.HasFlag(Traits.PlusAction)); var actionSplitsLeft = RealThis.Hand.Count(c => ShouldPlay(c) && c.Category.HasFlag(Categories.Action) && c.Traits.HasFlag(Traits.PlusMultipleActions)); var actionChainsLeft = RealThis.Hand.Count(c => ShouldPlay(c) && c.Category.HasFlag(Categories.Action) && c.Traits.HasFlag(Traits.PlusAction)) - actionSplitsLeft; // Adjust this number a bit -- TR/KC/Procession/Disciple/Crown can make an ActionChain an ActionSplit (end result of an extra 1 or 2 Actions left) actionSplitsLeft += Math.Min(RealThis.Hand[Cards.Base.TypeClass.ThroneRoom].Count, actionChainsLeft); actionSplitsLeft += Math.Min(RealThis.Hand[Cards.Base2ndEdition.TypeClass.ThroneRoom].Count, actionChainsLeft); actionSplitsLeft += Math.Min(2 * RealThis.Hand[Cards.Prosperity.TypeClass.KingsCourt].Count, actionChainsLeft); actionSplitsLeft += Math.Min(2 * RealThis.Hand[Cards.Prosperity2ndEdition.TypeClass.KingsCourt].Count, actionChainsLeft); actionSplitsLeft += Math.Min(RealThis.Hand[Cards.DarkAges.TypeClass.Procession].Count, actionChainsLeft); //actionSplitsLeft += Math.Min(RealThis.Hand[Cards.DarkAges2019Errata.TypeClass.Procession].Count, actionChainsLeft); actionSplitsLeft += Math.Min(RealThis.Hand[Cards.Adventures.TypeClass.Disciple].Count, actionChainsLeft); actionSplitsLeft += Math.Min(RealThis.Hand[Cards.Empires.TypeClass.Crown].Count, actionChainsLeft); // Only the first Crossroads counts for action splitting, *AND HOW* if it does! if (RealThis.Hand[Cards.Hinterlands.TypeClass.Crossroads].Any()) { actionSplitsLeft -= Math.Max(0, RealThis.Hand[Cards.Hinterlands.TypeClass.Crossroads].Count - 1); if (RealThis.CurrentTurn.CardsPlayed.Any(c => c is Cards.Hinterlands.Crossroads)) actionSplitsLeft--; else actionSplitsLeft++; } if (RealThis.Hand[Cards.Hinterlands2ndEdition.TypeClass.Crossroads].Any()) { actionSplitsLeft -= Math.Max(0, RealThis.Hand[Cards.Hinterlands2ndEdition.TypeClass.Crossroads].Count - 1); if (RealThis.CurrentTurn.CardsPlayed.Any(c => c is Cards.Hinterlands2ndEdition.Crossroads)) actionSplitsLeft--; else actionSplitsLeft++; } var actionPlayDeficit = ActionsAvailable() + actionSplitsLeft - 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.HasFlag(Categories.Curse) || c.Category.HasFlag(Categories.Ruins) || (c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure))) ?? choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); } } return hamletABestCard != null ? new ChoiceResult(new CardCollection { hamletABestCard }) : new ChoiceResult(new CardCollection()); } // Tunnel is always a great bet var hamletBBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.Tunnel); // Fairly simple analysis on the +1 Buy front if (RealThis.Buys < 2 && RealThis.Hand[Categories.Treasure].Sum(c => c.Benefit.Currency.Coin.Value) + RealThis.Currency.Coin.Value > 6) hamletBBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Curse) || c.Category.HasFlag(Categories.Ruins) || (c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure))); if (hamletBBestCard != null) return new ChoiceResult(new CardCollection { hamletBBestCard }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideHarbinger(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Find best card in our discard pile that isn't a Victory or Curse card (Action-Victory & Treasure-Victory cards are fine) var bestCardInDiscard = choice.Cards .Where(c => !c.Category.HasFlag(Categories.Victory) || !c.Category.HasFlag(Categories.Curse) || c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure)) .OrderByDescending(ComputeValueInDeck) .FirstOrDefault(); var averageDrawPileValue = RealThis.DrawPile.Count == 0 ? 0 : RealThis.DrawPile.LookThrough(c => true).Sum(c => ComputeValueInDeck(c)) / RealThis.DrawPile.Count; // If the best card in our discard pile is at least *close* to the average value of our deck, put it on top if (bestCardInDiscard != null && ComputeValueInDeck(bestCardInDiscard) > 0 && ComputeValueInDeck(bestCardInDiscard) > averageDrawPileValue * 0.85) return new ChoiceResult(new CardCollection { bestCardInDiscard }); return base.DecideHarbinger(choice); } protected override ChoiceResult DecideHauntedCastle(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } protected override ChoiceResult DecideHauntedMirror(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always do so if possible return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideHauntedWoods(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var hwCards = new CardCollection(choice.Cards); // Order them in roughly random order hwCards.Shuffle(); return new ChoiceResult(hwCards); } protected override ChoiceResult DecideHaunting(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Not the best option a lot of the time, but usable mostly return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideHaven(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (RealThis.Currency.Coin > 4) { var havenBestCard = choice.Cards.Where(c => c.Category.HasFlag(Categories.Action)).OrderByDescending(ComputeValueInDeck).FirstOrDefault(); if (havenBestCard == null) { // If there are none, pick a random non-Treasure card instead var havenNonTreasures = choice.Cards.Where(c => !c.Category.HasFlag(Categories.Treasure)).ToList(); return new ChoiceResult(new CardCollection { havenNonTreasures[_Game.RNG.Next(havenNonTreasures.Count)] }); } } else { // We don't have a lot of gold, so choose our biggest coin var havenTreasures = choice.Cards.Where(c => c.Category.HasFlag(Categories.Treasure)); // If there are no Treasures, try to grab Action cards instead if (!havenTreasures.Any()) { havenTreasures = choice.Cards.Where(c => c.Category.HasFlag(Categories.Action)); // Just pick any old card if (!havenTreasures.Any()) havenTreasures = choice.Cards; } return new ChoiceResult(new CardCollection { havenTreasures.ElementAt(_Game.RNG.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(_Game.RNG.Next(choice.Cards.Count())) }); } protected override ChoiceResult DecideHerald(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: for (var index = choice.Options.Count - 1; index >= 0; index--) { Currency overpayAmount = null; try { // Overpay by up to the amount of cards we have in our discard pile, // excluding Coppers, Curses, Ruins, Victory cards, & Shelters overpayAmount = new Currency(choice.Options[index].Text); if (overpayAmount.Potion.Value > 0) continue; if (DiscardPile.LookThrough(c => c.Type != Cards.Universal.TypeClass.Copper && c.Type != Cards.Universal.TypeClass.Curse && !c.Category.HasFlag(Categories.Ruins) && !c.Category.HasFlag(Categories.Shelter) && c.Category != Categories.Victory && c.Type != Cards.Hinterlands.TypeClass.Tunnel && c.Type != Cards.Hinterlands2ndEdition.TypeClass.Tunnel ).Count >= overpayAmount.Coin.Value) return new ChoiceResult(new List { choice.Options[index].Text }); } finally { if (!(overpayAmount is null)) overpayAmount.Dispose(); } } return base.DecideHerald(choice); case ChoiceType.Cards: // Find highest in-deck value cards to put on top // While ignoring Victory cards, Curses, & Shelters var heraldCards = new CardCollection(choice.Cards .Where(c => c.Category != Categories.Victory && c.Type != Cards.Universal.TypeClass.Curse && c.Type != Cards.Hinterlands.TypeClass.Tunnel && c.Type != Cards.Hinterlands2ndEdition.TypeClass.Tunnel && !c.Category.HasFlag(Categories.Shelter)) .OrderByDescending(ComputeValueInDeck) .Take(choice.Minimum)); // If this doesn't lead to enough cards, then include Shelters if (heraldCards.Count < choice.Minimum) heraldCards.AddRange(choice.Cards .Where(c => c.Category.HasFlag(Categories.Shelter)) .Take(choice.Minimum - heraldCards.Count)); // If this *still* doesn't include enough cards, include Victory cards if (heraldCards.Count < choice.Minimum) heraldCards.AddRange(choice.Cards .Where(c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) || c is Cards.Hinterlands.Tunnel || c is Cards.Hinterlands2ndEdition.Tunnel) .OrderByDescending(ComputeValueInDeck) .Take(choice.Minimum - heraldCards.Count)); // Uh... our deck is absolutely shit. Just take the first however many cards if (heraldCards.Count < choice.Minimum) heraldCards = new CardCollection(choice.Cards .OrderByDescending(ComputeValueInDeck) .Take(choice.Minimum)); return new ChoiceResult(heraldCards); default: return base.DecideHerald(choice); } } protected override ChoiceResult DecideHerbalist(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always choose the Treasure card that costs the most to put on top, except if it's Copper (F Copper) var bestCard = choice.Cards. OrderByDescending(card => card.Category.HasFlag(Categories.Prize) ? new Cost(7) : card.BaseCost). FirstOrDefault(); if (bestCard != null && bestCard.Type != Cards.Universal.TypeClass.Copper) return new ChoiceResult(new CardCollection { bestCard }); return base.DecideHerbalist(choice); } protected override ChoiceResult DecideHermit(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // Always choose to trash a Curse from Hermit if possible if (choice.Cards.Any(c => c is Cards.Universal.Curse)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Curse) }); // Always choose to trash a Ruins from Hermit if possible if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c.Category.HasFlag(Categories.Ruins)) }); return new ChoiceResult(new CardCollection()); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideHermit(choice); } } protected override ChoiceResult DecideHero(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideHideout(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideHornOfPlenty(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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 (GameProgressLeft > 0.35 || _Game.RNG.Next(5) <= 1) return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType().Where(supply => !supply.TopCard.Category.HasFlag(Categories.Victory)), null, false)); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideHorseTraders(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } protected override ChoiceResult DecideHostelry(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Treasures we're OK with discarding if it means we'll get a Horse for it return new ChoiceResult(new CardCollection(choice.Cards.Where( c => c is Cards.Universal.Copper || c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece || c is Cards.Empires.HumbleCastle || c is Cards.Empires.Rocks || c is Cards.Nocturne.Pasture ))); } protected override ChoiceResult DecideHuntingGrounds(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var hgChoice = choice.Options[0].Text; // 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 (RealThis._Game.Table.Duchy.Count == 0) hgChoice = choice.Options[1].Text; if (RealThis._Game.IsEndgameTriggered && RealThis._Game.Table.Estate.Count >= 3 && RealThis.CountAll(RealThis, c => c is Cards.Base.Gardens || c is Cards.Hinterlands.SilkRoad || c is Cards.Hinterlands2ndEdition.SilkRoad) > 0) hgChoice = choice.Options[1].Text; return new ChoiceResult(new List { hgChoice }); } protected override ChoiceResult DecideHuntingLodge(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // This needs a lot more logic, but this is a start at least // If our hand is empty, then Yes! if (RealThis.Hand.Count == 0) return new ChoiceResult(new List { choice.Options[0].Text }); return base.DecideHuntingLodge(choice); } protected override ChoiceResult DecideIllGottenGains(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always take the Copper -- more money = better, right? return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideImp(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cardsToPlay = new CardCollection(); var bestCard = FindBestCardToPlay(choice.Cards.Where(c => !(c is Cards.Base.Chapel) && !(c is Cards.Base.Library) && !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.SecretChamber) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Outpost) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.Tactician) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Outpost) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.Tactician) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.CountingHouse) && !(c is Cards.Prosperity.Forge) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity.Watchtower) && !(c is Cards.Prosperity2ndEdition.CountingHouse) && !(c is Cards.Prosperity2ndEdition.Forge) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Prosperity2ndEdition.Watchtower) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.JunkDealer) && !(c is Cards.DarkAges.Procession) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.DarkAges2019Errata.Procession) && !(c is Cards.Guilds.MerchantGuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.MerchantGuild) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Adventures.DistantLands) && !(c is Cards.Adventures.Duplicate) && !(c is Cards.Adventures.Champion) && !(c is Cards.Adventures.Teacher) && !(c is Cards.Adventures.RoyalCarriage) && !(c is Cards.Adventures2ndEdition.Champion) && !(c is Cards.Adventures2ndEdition.RoyalCarriage) && !(c is Cards.Empires.Sacrifice) && !(c is Cards.Nocturne.ZombieApprentice) && !(c is Cards.Nocturne.ZombieMason) && !(c is Cards.Menagerie.BountyHunter) && !(c is Cards.Menagerie.Displace) && !(c is Cards.Menagerie.Scrap) && !(c is Cards.Promotional.Dismantle) )); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(c => !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Nocturne.ZombieApprentice) && !(c is Cards.Nocturne.ZombieMason) && !(c is Cards.Menagerie.BountyHunter) && !(c is Cards.Menagerie.Displace) && !(c is Cards.Menagerie.Scrap) && !(c is Cards.Promotional.Dismantle) )); if (bestCard != null) cardsToPlay.Add(bestCard); return new ChoiceResult(cardsToPlay); } protected override ChoiceResult DecideImprove(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // This isn't great, but at least we should get rid of cards that are no longer useful (e.g. Sea Hag with no Curses) // And other trash-friendly cards like Fortress var trashable = FindBestCardsToTrash(choice.Cards, 1, true); if (trashable.Any()) return new ChoiceResult(new ItemCollection { trashable.First() }); return new ChoiceResult(new ItemCollection()); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } return base.DecideImprove(choice); } protected override ChoiceResult DecideInn(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.ChoiceOutcome == ChoiceOutcome.Discard) { return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } // Always select ALL Action cards we want to play return new ChoiceResult(new CardCollection(choice.Cards.Where(ShouldPlay))); } protected override ChoiceResult DecideInnovation(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always ??? return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideInventor(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideInvest(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // TODO -- Logic for Invest! // This is a bit of a hard decision. The first analysis that needs to be done is to identify // cards that are good "engine pieces" and/or useful cantrips. E.g. Forge is a powerful card, // but how many do you really want in your deck? Laboratory is almost always great in any deck var supplies = choice.Supplies.Values.OfType(); var bestSupply = FindBestCardForCost(supplies.Where(s => s.Count > 2 // Skip randomized piles && !(s.Randomizer is Cards.DarkAges.Knights || s.Randomizer is Cards.Empires.Castles) ), null, false); if (bestSupply == null) bestSupply = FindBestCardForCost(supplies.Where( s => s.Count > 1 // Skip randomized piles && !(s.Randomizer is Cards.DarkAges.Knights || s.Randomizer is Cards.Empires.Castles) ), null, false); if (bestSupply == null) bestSupply = FindBestCardForCost(supplies, null, false); return new ChoiceResult(bestSupply); } protected override ChoiceResult DecideIronmonger(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (IsCardOkForMeToDiscard(choice.Triggers.OfType().First())) return new ChoiceResult(new List { choice.Options[0].Text }); // Discard return new ChoiceResult(new List { choice.Options[1].Text }); // Put back } protected override ChoiceResult DecideIronworks(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var supplies = choice.Supplies.Values.OfType(); // Prioritize multi-type cards var bestMultiCard = FindBestCardForCost(supplies.Where(s => (s.Category.HasFlag(Categories.Action) && s.Category.HasFlag(Categories.Treasure)) || (s.Category.HasFlag(Categories.Treasure) && s.Category.HasFlag(Categories.Victory)) || (s.Category.HasFlag(Categories.Action) && s.Category.HasFlag(Categories.Victory)) ), null, false); return new ChoiceResult(bestMultiCard ?? FindBestCardForCost(supplies, null, false)); } protected override ChoiceResult DecideIsland(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var islandBestCard = choice.Cards .Where(c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) || c.Category.HasFlag(Categories.Curse)) .OrderByDescending(ComputeValueInDeck) .FirstOrDefault(c => true); if (islandBestCard != null) return new ChoiceResult(new CardCollection { islandBestCard }); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideJackOfAllTrades(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: if (choice.Triggers[0].Category.HasFlag(Categories.Victory) && !choice.Triggers[0].Category.HasFlag(Categories.Action) && !choice.Triggers[0].Category.HasFlag(Categories.Treasure) || choice.Triggers[0].Type == Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List { choice.Options[0].Text }); // Discard if (choice.Triggers[0].Type == Cards.Universal.TypeClass.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 (RealThis.Hand.Count < 5 && RealThis.Hand[Categories.Curse].Count == 0 && RealThis.Hand[Categories.Ruins].Count == 0) return new ChoiceResult(new List { choice.Options[1].Text }); // Put it back return new ChoiceResult(new List { choice.Options[1].Text }); // Discard } return new ChoiceResult(new List { choice.Options[1].Text }); // Put it back case ChoiceType.Cards: // Only ever trash Curses, Ruins, and SeaHags if there are no Curses left var joatCurse = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins) || (c is Cards.Seaside.SeaHag && !((ISupply)_Game.Table[Cards.Universal.TypeClass.Curse]).CanGain())); if (joatCurse != null) return new ChoiceResult(new CardCollection { joatCurse }); return new ChoiceResult(new CardCollection()); default: return base.DecideJackOfAllTrades(choice); } } protected override ChoiceResult DecideJester(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var type = choice.Triggers[0].Type; // HUGE list of cards & criteria for which player gets the card if (type == Cards.Universal.TypeClass.Curse || type == Cards.Universal.TypeClass.Copper || choice.Triggers[0].Category.HasFlag(Categories.Ruins) || (type == Cards.Base.TypeClass.Chapel && RealThis.CountAll(RealThis, c => c is Cards.Base.Chapel) > 2) || (type == Cards.Base.TypeClass.Mine && RealThis.CountAll(RealThis, c => c is Cards.Base.Mine) > 2) || (type == Cards.Base.TypeClass.Moneylender && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 4) || type == Cards.Base.TypeClass.Remodel || (type == Cards.Base.TypeClass.Workshop && _Game.Table.TableEntities.Count(kvp => kvp.Value.BaseCost == new Cost(4)) < 2) || (type == Cards.Base2ndEdition.TypeClass.Mine && RealThis.CountAll(RealThis, c => c is Cards.Base2ndEdition.Mine) > 2) || (type == Cards.Base2ndEdition.TypeClass.Moneylender && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 4) || type == Cards.Base2ndEdition.TypeClass.Remodel || (type == Cards.Intrigue.TypeClass.Coppersmith && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 4) || (type == Cards.Intrigue.TypeClass.Ironworks && _Game.Table.TableEntities.Count(kvp => kvp.Value.BaseCost == new Cost(4)) < 3) || type == Cards.Intrigue.TypeClass.Masquerade || type == Cards.Intrigue.TypeClass.TradingPost || (type == Cards.Intrigue2ndEdition.TypeClass.Ironworks && _Game.Table.TableEntities.Count(kvp => kvp.Value.BaseCost == new Cost(4)) < 3) || type == Cards.Intrigue2ndEdition.TypeClass.Masquerade || type == Cards.Intrigue2ndEdition.TypeClass.TradingPost || type == Cards.Seaside.TypeClass.Ambassador || type == Cards.Seaside.TypeClass.Lookout || type == Cards.Seaside.TypeClass.Salvager || (type == Cards.Seaside.TypeClass.SeaHag && !((ISupply)_Game.Table[Cards.Universal.TypeClass.Curse]).CanGain()) || type == Cards.Seaside.TypeClass.TreasureMap || type == Cards.Seaside2ndEdition.TypeClass.Ambassador || type == Cards.Seaside2ndEdition.TypeClass.Lookout || type == Cards.Seaside2ndEdition.TypeClass.Salvager || (type == Cards.Seaside2ndEdition.TypeClass.SeaHag && !((ISupply)_Game.Table[Cards.Universal.TypeClass.Curse]).CanGain()) || type == Cards.Seaside2ndEdition.TypeClass.TreasureMap || (type == Cards.Prosperity.TypeClass.Contraband && RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Contraband) > 3) || (type == Cards.Prosperity.TypeClass.CountingHouse && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 4) || type == Cards.Prosperity.TypeClass.Expand || type == Cards.Prosperity.TypeClass.Forge || (type == Cards.Prosperity.TypeClass.Mint && RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Mint) > 2) || (type == Cards.Prosperity.TypeClass.Talisman && (_Game.Table.TableEntities.Count(kvp => kvp.Value.BaseCost == new Cost(4)) < 3 || RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Talisman) > 2)) || type == Cards.Prosperity.TypeClass.TradeRoute || (type == Cards.Prosperity2ndEdition.TypeClass.CountingHouse && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 4) || type == Cards.Prosperity2ndEdition.TypeClass.Expand || type == Cards.Prosperity2ndEdition.TypeClass.Forge || (type == Cards.Prosperity2ndEdition.TypeClass.Talisman && (_Game.Table.TableEntities.Count(kvp => kvp.Value.BaseCost == new Cost(4)) < 3 || RealThis.CountAll(RealThis, c => c is Cards.Prosperity2ndEdition.Talisman) > 2)) || type == Cards.Prosperity2ndEdition.TypeClass.TradeRoute || (type == Cards.Cornucopia.TypeClass.HornOfPlenty && RealThis.CountAll(RealThis, c => c is Cards.Cornucopia.HornOfPlenty) > 3) || type == Cards.Cornucopia.TypeClass.Remake || (type == Cards.Cornucopia.TypeClass.YoungWitch && !((ISupply)_Game.Table[Cards.Universal.TypeClass.Curse]).CanGain()) || (type == Cards.Cornucopia2ndEdition.TypeClass.HornOfPlenty && RealThis.CountAll(RealThis, c => c is Cards.Cornucopia2ndEdition.HornOfPlenty) > 3) || type == Cards.Cornucopia2ndEdition.TypeClass.Remake || (type == Cards.Cornucopia2ndEdition.TypeClass.YoungWitch && !((ISupply)_Game.Table[Cards.Universal.TypeClass.Curse]).CanGain()) || type == Cards.Hinterlands.TypeClass.Develop || type == Cards.Hinterlands2ndEdition.TypeClass.Develop || type == Cards.DarkAges.TypeClass.Procession || type == Cards.DarkAges.TypeClass.Rats || type == Cards.DarkAges.TypeClass.Rebuild || type == Cards.DarkAges2ndEdition.TypeClass.Rats || type == Cards.DarkAges2ndEdition.TypeClass.Rebuild //|| type == Cards.DarkAges2019Errata.TypeClass.Procession || type == Cards.Guilds.TypeClass.Masterpiece || type == Cards.Guilds.TypeClass.Stonemason || type == Cards.Guilds2ndEdition.TypeClass.Masterpiece || type == Cards.Guilds2ndEdition.TypeClass.Stonemason || type == Cards.Empires.TypeClass.Sacrifice || type == Cards.Nocturne.TypeClass.ZombieMason || type == Cards.Renaissance.TypeClass.Hideout || type == Cards.Renaissance.TypeClass.Priest || type == Cards.Renaissance.TypeClass.Recruiter || type == Cards.Renaissance.TypeClass.Research || type == Cards.Menagerie.TypeClass.Scrap //|| type == Cards.Promotional.TypeClass.Dismantle ) return new ChoiceResult(new List { choice.Options[1].Text }); else return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideJourneyman(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Sort by likelihood of this item showing up IEnumerable costItems = choice.Cards.Union(choice.Supplies.Select(kvp => kvp.Value).OfType()).OrderByDescending(item => CountAll(this, c => c.Type == item.Type, onlyCurrentlyDrawable: true)); // Choose the most problematic to draw foreach (var item in costItems) { if (item is Cards.Universal.Curse || item.Category.HasFlag(Categories.Ruins) || item is Cards.Hinterlands.Tunnel || item is Cards.Hinterlands2ndEdition.Tunnel || (item.Category.HasFlag(Categories.Victory) && !item.Category.HasFlag(Categories.Action) && !item.Category.HasFlag(Categories.Treasure)) || (ActionsAvailable() == 0 && item.Category.HasFlag(Categories.Action) && !item.Category.HasFlag(Categories.Treasure)) ) { var itemType = item.GetType(); if (item is ISupply iSupply) return new ChoiceResult(iSupply); if (itemType.IsSubclassOf(typeof(Card))) return new ChoiceResult(new CardCollection { (Card)item }); throw new Exception("What is this thing?"); } } return base.DecideJourneyman(choice); } protected override ChoiceResult DecideJunkDealer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideKingsCourt(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierExclusionPredicate())); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierDestructiveExclusionPredicate())); if (bestCard != null) return new ChoiceResult(new CardCollection { bestCard }); // Don't play anything return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideLibrary(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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 (ActionsAvailable() > 0 || !ShouldPlay(choice.Triggers.OfType().First())) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideLegionary(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // Always choose to reveal return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 2))); } return base.DecideLegionary(choice); } protected override ChoiceResult DecideLoan(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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/Masterpiece 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.Triggers[0].Type == Cards.Universal.TypeClass.Copper || ((choice.Triggers[0].Type == Cards.Universal.TypeClass.Silver || choice.Triggers[0].Type == Cards.Prosperity.TypeClass.Talisman || choice.Triggers[0].Type == Cards.Prosperity.TypeClass.Quarry || choice.Triggers[0].Type == Cards.Prosperity2ndEdition.TypeClass.Talisman || choice.Triggers[0].Type == Cards.Guilds.TypeClass.Masterpiece || choice.Triggers[0].Type == Cards.Guilds2ndEdition.TypeClass.Masterpiece) && RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Platinum) > 0 && RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Venture) > 3) || ((choice.Triggers[0].Type == Cards.Prosperity.TypeClass.Loan || choice.Triggers[0].Type == Cards.Prosperity2ndEdition.TypeClass.Loan) && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 3)) return new ChoiceResult(new List { choice.Options[1].Text }); // Trash return new ChoiceResult(new List { choice.Options[0].Text }); // Discard } protected override ChoiceResult DecideLocusts(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideLookout(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == Resource.ChooseACardToTrash) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideLostArts(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var maxCards = 0; var maxCardsSupplies = new List(); var supplies = choice.Supplies.Values.OfType(); foreach (var supply in supplies) { var count = RealThis.CountAll(RealThis, c => supply.Types.Contains(c.Type)); if (count > maxCards) maxCards = count; if (count == maxCards) maxCardsSupplies.Add(supply); } return new ChoiceResult(FindBestCardForCost(maxCardsSupplies.Any() ? maxCardsSupplies : supplies, null, false)); } protected override ChoiceResult DecideLostInTheWoods(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always do it return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideLurker(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: if (!_Game.Table.Trash.Any(c => c.Category.HasFlag(Categories.Action) && ShouldBuy(c.Type))) return new ChoiceResult(new List { choice.Options[0].Text }); // 90% of the time, gain an Action card from the trash if (_Game.RNG.Next(0, 10) < 8) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } return base.DecideLurker(choice); } protected override ChoiceResult DecideMandarin(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Not always the best decision, but for now, it's the easiest if (choice.Text.StartsWith(Resource.TopdeckCard, StringComparison.InvariantCulture)) { return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } var cards = new CardCollection(choice.Cards); // Order them in roughly random order cards.Shuffle(); return new ChoiceResult(cards); } protected override ChoiceResult DecideMarch(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cards = new CardCollection(); var cardToPlay = FindBestCardToPlay(choice.Cards); if (cardToPlay != null) cards.Add(cardToPlay); return new ChoiceResult(cards); } protected override ChoiceResult DecideMargrave(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult DecideMasquerade(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceOutcome) { case ChoiceOutcome.Select: var masqBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Curse)); if (masqBestCard == null) masqBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Ruins)); if (masqBestCard == null) masqBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); if (masqBestCard == null) masqBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Base.Chapel || c is Cards.Base.Moneylender || c is Cards.Base.Remodel || c is Cards.Base2ndEdition.Moneylender || c is Cards.Intrigue.Masquerade || c is Cards.Intrigue.TradingPost || c is Cards.Intrigue.Upgrade || c is Cards.Intrigue2ndEdition.Masquerade || c is Cards.Seaside.Lookout || c is Cards.Seaside.Salvager || c is Cards.Seaside2ndEdition.Lookout || c is Cards.Seaside2ndEdition.Salvager || c is Cards.Alchemy.Transmute || c is Cards.Prosperity.Expand || c is Cards.Prosperity.Forge || c is Cards.Prosperity.TradeRoute || c is Cards.Prosperity2ndEdition.Expand || c is Cards.Prosperity2ndEdition.Forge || c is Cards.Prosperity2ndEdition.TradeRoute || c is Cards.Cornucopia.Remake || c is Cards.Hinterlands.Develop || c is Cards.Cornucopia2ndEdition.Remake || c is Cards.Hinterlands2ndEdition.Develop || c is Cards.DarkAges.Hovel || c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges.Rats || c is Cards.DarkAges.Rebuild || c is Cards.DarkAges2ndEdition.OvergrownEstate || c is Cards.DarkAges2ndEdition.Rats || c is Cards.DarkAges2ndEdition.Rebuild || c is Cards.Guilds.Stonemason || c is Cards.Guilds2ndEdition.Stonemason ); if (masqBestCard == null) masqBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); // 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 + 1.2 * c.BaseCost.Debt.Value).FirstOrDefault(); if (masqBestCard != null) return new ChoiceResult(new CardCollection { masqBestCard }); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); case ChoiceOutcome.Trash: var masqCurse = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Curse)); if (masqCurse != null) return new ChoiceResult(new CardCollection { masqCurse }); return new ChoiceResult(new CardCollection()); } return base.DecideMasquerade(choice); } protected override ChoiceResult DecideMastermind(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierExclusionPredicate())); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierDestructiveExclusionPredicate())); if (bestCard != null) return new ChoiceResult(new CardCollection { bestCard }); // Don't play anything return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideMasterpiece(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always overpay by as much as we can return new ChoiceResult(new List { choice.Options[choice.Options.Count - 1].Text }); } protected override ChoiceResult DecideMercenary(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var choiceMercenary = choice.Options[1].Text; var trashableCards = FindBestCardsToTrash(RealThis.Hand, 2, true); if (trashableCards.Count() >= 2) choiceMercenary = choice.Options[0].Text; return new ChoiceResult(new List { choiceMercenary }); case ChoiceType.Cards: if (choice.Text == Resource.Trash2) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(RealThis.Hand, 2, false))); if (choice.Text == Resource.DiscardDownTo3Cards) return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); return base.DecideMercenary(choice); default: return base.DecideMercenary(choice); } } protected override ChoiceResult DecideMessenger(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // Never put deck into discard pile later in the game -- we don't want those Victory cards back in our deck sooner // This is very suspect. A better algorithm could be done to figure out if our deck is better than our discard pile/hand if (GameProgressLeft < 0.30) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideMessenger(choice); } } protected override ChoiceResult DecideMilitia(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult DecideMill(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var cardsToDiscard = new CardCollection(); // Allowable cards to discard if (RealThis.Hand.Count(c => c is Cards.Universal.Curse || c is Cards.Universal.Copper || c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece || c.Category.HasFlag(Categories.Ruins) || !ShouldBuy(c.Type) ) >= 2) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } return base.DecideMill(choice); } protected override ChoiceResult DecideMine(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: Card mineCard = null; var mineableCards = choice.Cards.ToList(); if (choice.Minimum == 0) mineableCards = mineableCards.Where(c => !(c is Cards.Prosperity.Platinum)).ToList(); if (_Game.Table.TableEntities.ContainsKey(Cards.Prosperity.TypeClass.Platinum) && ((ISupply)_Game.Table.TableEntities[Cards.Prosperity.TypeClass.Platinum]).CanGain()) mineCard = mineableCards.FirstOrDefault(c => c is Cards.Universal.Gold); else if (choice.Minimum == 0) mineableCards = mineableCards.Where(c => !(c is Cards.Universal.Gold)).ToList(); if (mineCard == null) mineCard = mineableCards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); if (mineCard == null) mineCard = mineableCards.FirstOrDefault(c => c is Cards.Universal.Silver); if (mineCard == null) mineCard = mineableCards.FirstOrDefault(c => c is Cards.Universal.Copper); if (mineCard == null) mineCard = mineableCards.FirstOrDefault(c => c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains); if (mineCard == null) mineCard = mineableCards.FirstOrDefault(c => c is Cards.Empires.Rocks); if (mineCard == null) // Pick a random Treasure at this point mineCard = mineableCards[_Game.RNG.Next(mineableCards.Count)]; return new ChoiceResult(new CardCollection { mineCard }); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideMine(choice); } } protected override ChoiceResult DecideMiningVillage(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Trash if between 4 & 7 Coins available (?? Odd choice) if (RealThis.Currency.Coin > 3 && RealThis.Currency.Coin < 8) return new ChoiceResult(new List { choice.Options[0].Text }); // Yes return new ChoiceResult(new List { choice.Options[1].Text }); // No } protected override ChoiceResult DecideMinion(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Gain coins if we have another Minion in hand // Gain coins if between 4 & 7 Coins available (?? Odd choice) if (RealThis.Hand[c => c is Cards.Intrigue.Minion].Any() || (RealThis.Currency.Coin > 3 && RealThis.Currency.Coin < 8)) return new ChoiceResult(new List { choice.Options[0].Text }); // +2 Coins return new ChoiceResult(new List { choice.Options[1].Text }); // Discard Hand } protected override ChoiceResult DecideMint(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always choose the Treasure card that costs the most to duplicate Card bestCard = null; foreach (var card in choice.Cards) { if (RealThis._Game.Table.TableEntities.ContainsKey(card) && ((ISupply)RealThis._Game.Table.TableEntities[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 DecideMiser(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (RealThis.PlayerMats[Cards.Adventures.TypeClass.TavernMat].Count(c => c is Cards.Universal.Copper) > _Game.RNG.Next(1, 4) || RealThis.Hand[Cards.Universal.TypeClass.Copper].Count == 0) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideMonastery(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always choose to trash a Curse if possible if (choice.Cards.Any(c => c is Cards.Universal.Curse)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Curse) }); // Always choose to trash a Ruins if possible if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c.Category.HasFlag(Categories.Ruins)) }); // Always choose to trash Hovel if possible if (choice.Cards.Any(c => c is Cards.DarkAges.Hovel)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.DarkAges.Hovel) }); // If there are no Curses left, choose to trash Sea Hag or Familiar if possible if (_Game.Table.Curse.Count == 0 && choice.Cards.Any(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar) }); // Always choose to trash Copper if we have at least 2 Golds or at least 5 Silvers if ((RealThis.CountAll(RealThis, c => c is Cards.Universal.Gold) >= 2 || RealThis.CountAll(RealThis, c => c is Cards.Universal.Silver) >= 5) && choice.Cards.Any(c => c is Cards.Universal.Copper)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Copper) }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideMoneylender(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always "Yes" return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideMountainPass(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var result = base.DecideMountainPass(choice); // We'll choose randomly for the time being, unless it's the player who bought the Province, then bid // 1 more than the previous bidder *if* we're going to not pass on it if (RealThis == _Game.ActivePlayer && result.Options[0] != choice.Options[0].Text) result.Options[0] = choice.Options[1].Text; // This logic needs to be done. 8 VP is a lot and is probably worth quite a few Debt tokens return result; } protected override ChoiceResult DecideMountainVillage(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection { choice.Cards.OrderByDescending(ComputeValueInDeck).First() }); } protected override ChoiceResult DecideMountebank(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Discard curse if I don't have a Trader in my hand -- 2 Silvers are better than no Curse card in hand if (RealThis.Hand[c => c is Cards.Hinterlands.Trader || c is Cards.Hinterlands2ndEdition.Trader].Any()) return new ChoiceResult(new List { choice.Options[1].Text }); // Otherwise, just discard the Curse return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideMystic(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cardsCount = new Dictionary(); foreach (var type in CardsGained) cardsCount[type] = (int)Math.Pow(RealThis.CountAll(RealThis, c => c.Type == type, false, true), 2); // Choose one at random, with a probability based on the cards left to be able to draw var indexChosen = _Game.RNG.Next(cardsCount.Sum(kvp => kvp.Value)); Card mysticCard = null; foreach (var type in cardsCount.Keys) { if (cardsCount[type] == 0) continue; if (indexChosen < cardsCount[type]) { var mysticSupply = choice.Supplies.Select(kvp => kvp.Value).OfType().FirstOrDefault(s => s.Type == type); if (mysticSupply != null) return new ChoiceResult(mysticSupply); mysticCard = choice.Cards.FirstOrDefault(c => c.Type == type); break; } indexChosen -= cardsCount[type]; } if (mysticCard != null) return new ChoiceResult(new CardCollection { mysticCard }); var supplies = choice.Supplies.Select(kvp => kvp.Value).OfType(); return new ChoiceResult(supplies.ElementAt(_Game.RNG.Next(supplies.Count()))); } protected override ChoiceResult DecideNativeVillage(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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 (RealThis.PlayerMats[Cards.Seaside.TypeClass.NativeVillageMat].Count > _Game.RNG.Next(2, 5) && RealThis.Hand[c => c is Cards.Seaside.NativeVillage || c is Cards.Seaside2ndEdition.NativeVillage].Count == 0) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideNavigator(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // Untested Navigator logic // Basically, all this is doing is seeing if the average "in-deck value" of the private cards is more than the average // "in-deck value" of the entire deck, with Victory cards having a value of 0. var totalDeckValue = RealThis.SumAll(RealThis, c => true, c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) ? 0 : (c is Card cCard ? ComputeValueInDeck(cCard) : 0)); var averageDeckValue = totalDeckValue / RealThis.CountAll(); var averageRevealedValue = Private.Sum(c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) ? 0 : ComputeValueInDeck(c)) / Private.Count; // A modest fudge factor may be needed here. If the private cards are borderline, then we should maybe just keep them if (averageRevealedValue + 0.5 > averageDeckValue) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); } return base.DecideNavigator(choice); } protected override ChoiceResult DecideNecromancer(Choice choice) { // Certain cards are worse when played from the Trash, e.g. Highway & Goons, but they still have good benefit // This method doesn't account for those skewed values yet Contract.Requires(choice != null, "choice cannot be null"); var bestCard = FindBestCardToPlay(choice.Cards.Where(c => !(c is Cards.Base.Chapel) && !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.MiningVillage) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.Forge) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity2ndEdition.Forge) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.JunkDealer) && !(c is Cards.DarkAges.Madman) && !(c is Cards.DarkAges.Procession) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Madman) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.DarkAges2019Errata.Procession) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Adventures.DistantLands) && !(c is Cards.Adventures.Duplicate) && !(c is Cards.Adventures.Teacher) && !(c is Cards.Adventures.RoyalCarriage) && !(c is Cards.Adventures2ndEdition.RoyalCarriage) && !(c is Cards.Empires.Sacrifice) && !(c is Cards.Nocturne.Necromancer) && !(c is Cards.Nocturne.Pixie) && !(c is Cards.Nocturne.ZombieApprentice) && !(c is Cards.Nocturne.ZombieMason) && !(c is Cards.Renaissance.Hideout) && !(c is Cards.Renaissance.Priest) && !(c is Cards.Renaissance.Recruiter) && !(c is Cards.Renaissance.Research) && !(c is Cards.Menagerie.Scrap) )); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(c => !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Nocturne.ZombieApprentice) && !(c is Cards.Nocturne.ZombieMason) && !(c is Cards.Renaissance.Hideout) && !(c is Cards.Renaissance.Priest) && !(c is Cards.Renaissance.Recruiter) && !(c is Cards.Renaissance.Research) && !(c is Cards.Menagerie.Scrap) )); if (bestCard != null) return new ChoiceResult(new CardCollection { bestCard }); // Decide at random I guess return base.DecideNecromancer(choice); } protected override ChoiceResult DecideNightWatchman(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceOutcome) { case ChoiceOutcome.Discard: // Grab all cards that we don't really care about return new ChoiceResult(new CardCollection(choice.Cards.Where(IsCardOkForMeToDiscard))); case ChoiceOutcome.Select: var cartCards = new CardCollection(choice.Cards); // Order them in roughly random order cartCards.Shuffle(); return new ChoiceResult(cartCards); } return base.DecideNightWatchman(choice); } protected override ChoiceResult DecideNobleBrigand(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); } protected override ChoiceResult DecideNobles(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Choose +2 Actions only if there are fewer Actions than Action cards we want to play if (RealThis.Hand.Count(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c)) > ActionsAvailable()) return new ChoiceResult(new List { choice.Options[1].Text }); // +2 Actions return new ChoiceResult(new List { choice.Options[0].Text }); // +3 Cards } protected override ChoiceResult DecideOasis(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideOldWitch(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always "Yes" return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideOpulentCastle(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(choice.Cards.Where(c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure)))); } protected override ChoiceResult DecideOracle(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: if (choice.PlayerSource == RealThis) { var totalDeckDiscardability = RealThis.SumAll(RealThis, c => true, c => c is Card ? ComputeDiscardValue((Card)c) : 0, true, true); var totalCards = RealThis.CountAll(RealThis, c => true, true, true); var cardsDiscardability = choice.Triggers.OfType().Sum(c => ComputeDiscardValue(c)); // If it's better to keep these cards than discard them if (cardsDiscardability / choice.Triggers.Count >= totalDeckDiscardability / totalCards) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } else { var totalDeckDiscardability = choice.PlayerSource.SumAll(RealThis, c => true, c => c is Card ? ComputeDiscardValue((Card)c) : 0, onlyCurrentlyDrawable: true); var totalCards = choice.PlayerSource.CountAll(RealThis, c => true, onlyCurrentlyDrawable: true); var cardsDiscardability = choice.Triggers.OfType().Sum(c => ComputeDiscardValue(c)); // If it's better to discard these cards than keep them if (cardsDiscardability / choice.Triggers.Count >= totalDeckDiscardability / totalCards) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); } case ChoiceType.Cards: var oracleCards = new CardCollection(choice.Cards); // Order them in roughly random order oracleCards.Shuffle(); return new ChoiceResult(oracleCards); default: return base.DecideOracle(choice); } } protected override ChoiceResult DecideOverlord(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecidePathfinding(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var maxCards = 0; var maxCardsSupplies = new List(); var supplies = choice.Supplies.Values.OfType(); foreach (var supply in supplies) { var count = RealThis.CountAll(RealThis, c => supply.Types.Contains(c.Type)); if (count > maxCards) maxCards = count; if (count == maxCards) maxCardsSupplies.Add(supply); } return new ChoiceResult(FindBestCardForCost(maxCardsSupplies.Any() ? maxCardsSupplies : supplies, null, false)); } protected override ChoiceResult DecidePatrol(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var patrolCards = new CardCollection(choice.Cards); // Order them in roughly random order patrolCards.Shuffle(); return new ChoiceResult(patrolCards); } protected override ChoiceResult DecidePawn(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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 var pawnChoices = new List { choice.Options[3].Text }; // +1 Coin if (RealThis.Hand.Count(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c)) > ActionsAvailable()) pawnChoices.Add(choice.Options[1].Text); // +1 Action else pawnChoices.Add(choice.Options[0].Text); // +1 Card return new ChoiceResult(pawnChoices); } protected override ChoiceResult DecidePearlDiver(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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.Triggers[0].Category.HasFlag(Categories.Victory) && !choice.Triggers[0].Category.HasFlag(Categories.Action) && !choice.Triggers[0].Category.HasFlag(Categories.Treasure) || choice.Triggers[0].Category.HasFlag(Categories.Ruins) || choice.Triggers[0].Type == Cards.Universal.TypeClass.Curse || choice.Triggers[0].Type == Cards.Universal.TypeClass.Copper || choice.Triggers[0].Type == Cards.Hinterlands.TypeClass.Tunnel || choice.Triggers[0].Type == Cards.Hinterlands2ndEdition.TypeClass.Tunnel || choice.Triggers[0].Type == Cards.DarkAges.TypeClass.OvergrownEstate || choice.Triggers[0].Type == Cards.DarkAges.TypeClass.Hovel || choice.Triggers[0].Type == Cards.DarkAges2ndEdition.TypeClass.OvergrownEstate) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideArena(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecidePilgrimage(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var shouldGain = new CardCollection(); foreach (var card in choice.Cards) { if (!ShouldBuy(card.Type)) continue; var supply = _Game.Table.FindSupplyPileByCard(card); if (supply != null && supply.CanGain(card.Type)) shouldGain.Add(card); } return new ChoiceResult(new CardCollection(shouldGain.OrderByDescending(c => c.BaseCost).Take(3))); } protected override ChoiceResult DecidePillage(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // First priority is Platinum if (choice.Cards.Any(c => c is Cards.Prosperity.Platinum)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Prosperity.Platinum) }); // Next priority is King's Court if the player has Action cards other than KC/TR if (choice.Cards.Any(c => c is Cards.Prosperity.KingsCourt) && choice.Cards.Any(c => c.Category.HasFlag(Categories.Action) && c.Type != Cards.Prosperity.TypeClass.KingsCourt && c.Type != Cards.Base.TypeClass.ThroneRoom && c.Type != Cards.Base2ndEdition.TypeClass.ThroneRoom && c.Type != Cards.DarkAges.TypeClass.Procession //&& c.Type != Cards.DarkAges2019Errata.TypeClass.Procession && c.Type != Cards.Adventures.TypeClass.Disciple && c.Type != Cards.Adventures.TypeClass.RoyalCarriage && c.Type != Cards.Adventures2ndEdition.TypeClass.RoyalCarriage && c.Type != Cards.Empires.TypeClass.Crown)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Prosperity.KingsCourt || c is Cards.Prosperity2ndEdition.KingsCourt) }); // Next priority is 5-cost+ Attack cards if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Attack) && c.BaseCost.Coin >= 5)) return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards.Where(c => c.Category.HasFlag(Categories.Attack) && c.BaseCost.Coin >= 5), 1))); // Next priority is Gold if (choice.Cards.Any(c => c is Cards.Universal.Gold)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Gold) }); // Next priority is 5-cost+ Action/Treasure cards (other than Ill-Gotten Gains) if (choice.Cards.Any(c => (c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure)) && c.BaseCost.Coin >= 5 && c.Type != Cards.Hinterlands.TypeClass.IllGottenGains && c.Type != Cards.Hinterlands2ndEdition.TypeClass.IllGottenGains)) return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards.Where(c => (c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure)) && c.BaseCost.Coin >= 5 && c.Type != Cards.Hinterlands.TypeClass.IllGottenGains && c.Type != Cards.Hinterlands2ndEdition.TypeClass.IllGottenGains), 1))); // Next priority is any remaining Attack cards if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Attack))) return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards.Where(c => c.Category.HasFlag(Categories.Attack)), 1))); // Next priority is any remaining Action/Treasure cards if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure))) return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards.Where(c => c.Category.HasFlag(Categories.Action) || c.Category.HasFlag(Categories.Treasure)), 1))); // Final fall-through return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); } protected override ChoiceResult DecidePirateShip(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var ePlayers = RealThis._Game.GetPlayersStartingWithActiveEnumerator(); ePlayers.MoveNext(); var blockedCount = 0; while (ePlayers.MoveNext()) { var attackee = ePlayers.Current; if (attackee.SetAside[Cards.Seaside.TypeClass.Lighthouse].Any() || (KnownPlayerHands.ContainsKey(attackee.UniqueId) && KnownPlayerHands[attackee.UniqueId].Any(c => c is Cards.Base.Moat))) blockedCount++; } // Take the Pirate Ship tokens if all attacks have been blocked -- no point in attacking if (blockedCount == RealThis._Game.Players.Count - 1) return new ChoiceResult(new List { choice.Options[1].Text }); // Steal coins until I have at least 3 Pirate Ship tokens on my mat if (RealThis.TokenPiles[Cards.Seaside.TypeClass.PirateShipToken].Count > 3) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); default: return base.DecideRemodel(choice); } } protected override ChoiceResult DecidePixie(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Might need to do some more analysis on when we should be trashing Pixie // Trash for The Flame's Gift if we've got 2 Curses in our hand if (choice.Triggers.Any(t => t is Cards.Nocturne.TheFlamesGift) && RealThis.Hand.Count(c => c is Cards.Universal.Curse) >= 2) return new ChoiceResult(new List { choice.Options[0].Text }); // Trash for The Moon's Gift if we've got 2 good cards in our discard pile if (choice.Triggers.Any(t => t is Cards.Nocturne.TheMoonsGift) && RealThis.DiscardPile.LookThrough(hC => hC.Category.HasFlag(Categories.Action) || hC.Category.HasFlag(Categories.Treasure) || hC.Category.HasFlag(Categories.Night)) .Select(hC => ComputeValueInDeck(hC)).Count(v => v >= 6.0) >= 2) return new ChoiceResult(new List { choice.Options[0].Text }); // Always trash for The Sea's Gift or The Swamp's Gift if (choice.Triggers.Any(t => t is Cards.Nocturne.TheSeasGift || t is Cards.Nocturne.TheSwampsGift)) return new ChoiceResult(new List { choice.Options[0].Text }); // Trash for The Sun's Gift if we've got 8+ cards in our deck if (choice.Triggers.Any(t => t is Cards.Nocturne.TheSunsGift) && RealThis.DrawPile.Count >= 8) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecidePlan(Choice choice) { // TODO -- This should select cheap-ish cards we want a lot of (e.g. Caravan & Hamlet, not Goons or Lookout) Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecidePlaza(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always discard Copper var plazaBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); if (plazaBestCard == null && (RealThis.Hand[Cards.Alchemy.TypeClass.Potion].Count > 1 || !_Game.Table.TableEntities.Values.OfType().Any(s => s.BaseCost.Potion > 0 && s.CanGain()))) plazaBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Alchemy.Potion); if (plazaBestCard == null && RealThis.Hand[Cards.Hinterlands.TypeClass.FoolsGold].Count == 1) plazaBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.FoolsGold || c is Cards.Hinterlands2ndEdition.FoolsGold); if (plazaBestCard == null && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 4) plazaBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan); if (plazaBestCard == null) plazaBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); if (plazaBestCard != null) return new ChoiceResult(new CardCollection { plazaBestCard }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecidePoacher(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Minimum))); } protected override ChoiceResult DecidePooka(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); bool trashablePred(Card c) => c is Cards.Universal.Copper || c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece; if (choice.Cards.Any(trashablePred) && (RealThis.CountAll(RealThis, cG => cG is Cards.Universal.Gold) >= 2 || RealThis.CountAll(RealThis, cG => cG is Cards.Universal.Silver) >= 5)) return new ChoiceResult(new CardCollection { choice.Cards.FirstOrDefault(trashablePred) }); return base.DecidePooka(choice); } protected override ChoiceResult DecidePoverty(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); } protected override ChoiceResult DecidePriest(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecidePrince(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); Card cardToSetAside = null; double bestScore = 0f; var cardCap = new Currency(4); foreach (var cardTest in RealThis.Hand[c => c.Category.HasFlag(Categories.Action) && RealThis._Game.ComputeCost(c) <= cardCap]) { // Skip all Duration cards if (cardTest.Category.HasFlag(Categories.Duration)) continue; // Skip the following cards if (cardTest is Cards.Base.Feast || cardTest is Cards.Base.Remodel || cardTest is Cards.Intrigue.TradingPost || cardTest is Cards.Intrigue.Upgrade || cardTest is Cards.Seaside.Embargo || cardTest is Cards.Seaside.Island || cardTest is Cards.Seaside.Lookout || cardTest is Cards.Seaside.Salvager || cardTest is Cards.Seaside.TreasureMap || cardTest is Cards.Seaside2ndEdition.Embargo || cardTest is Cards.Seaside2ndEdition.Island || cardTest is Cards.Seaside2ndEdition.Lookout || cardTest is Cards.Seaside2ndEdition.Salvager || cardTest is Cards.Seaside2ndEdition.TreasureMap || cardTest is Cards.Seaside2019Errata.Embargo || cardTest is Cards.Alchemy.Apprentice || cardTest is Cards.Alchemy2ndEdition.Apprentice || cardTest is Cards.Prosperity.Expand || cardTest is Cards.Prosperity.Forge || cardTest is Cards.Prosperity.TradeRoute || cardTest is Cards.Prosperity2ndEdition.Expand || cardTest is Cards.Prosperity2ndEdition.Forge || cardTest is Cards.Prosperity2ndEdition.TradeRoute || cardTest is Cards.Cornucopia.Remake || cardTest is Cards.Cornucopia2ndEdition.Remake || cardTest is Cards.Hinterlands.Develop || cardTest is Cards.Hinterlands.Trader || cardTest is Cards.Hinterlands2ndEdition.Develop || cardTest is Cards.Hinterlands2ndEdition.Trader || cardTest is Cards.DarkAges.DeathCart || cardTest is Cards.DarkAges.Forager || cardTest is Cards.DarkAges.JunkDealer || cardTest is Cards.DarkAges.Madman || cardTest is Cards.DarkAges.Pillage || cardTest is Cards.DarkAges.Procession || cardTest is Cards.DarkAges.Rats || cardTest is Cards.DarkAges.Rebuild || cardTest is Cards.DarkAges2ndEdition.Forager || cardTest is Cards.DarkAges2ndEdition.Madman || cardTest is Cards.DarkAges2ndEdition.Pillage || cardTest is Cards.DarkAges2ndEdition.Rats || cardTest is Cards.DarkAges2ndEdition.Rebuild || cardTest is Cards.DarkAges2019Errata.DeathCart || cardTest is Cards.DarkAges2019Errata.Pillage || cardTest is Cards.DarkAges2019Errata.Procession || cardTest is Cards.Guilds.Butcher || cardTest is Cards.Guilds.Stonemason || cardTest is Cards.Guilds2ndEdition.Butcher || cardTest is Cards.Guilds2ndEdition.Stonemason || cardTest.Category.HasFlag(Categories.Reserve) || cardTest.Category.HasFlag(Categories.Traveller) || cardTest is Cards.Adventures.BridgeTroll || cardTest is Cards.Adventures.CaravanGuard || cardTest is Cards.Adventures.Champion || cardTest is Cards.Adventures.Dungeon || cardTest is Cards.Adventures.Gear || cardTest is Cards.Adventures.HauntedWoods || cardTest is Cards.Adventures.Hireling || cardTest is Cards.Adventures.SwampHag || cardTest is Cards.Adventures.Teacher || cardTest is Cards.Empires.Catapult || cardTest is Cards.Empires.Encampment || cardTest is Cards.Empires.Enchantress || cardTest is Cards.Empires.FarmersMarket || cardTest is Cards.Empires.Sacrifice || cardTest is Cards.Empires.SmallCastle || cardTest is Cards.Empires.Temple || cardTest is Cards.Nocturne.TragicHero || cardTest is Cards.Nocturne.Wish || cardTest is Cards.Renaissance.ActingTroupe || cardTest is Cards.Renaissance.CargoShip || cardTest is Cards.Renaissance.Experiment || cardTest is Cards.Renaissance.Hideout || cardTest is Cards.Renaissance.Priest || cardTest is Cards.Renaissance.Research || cardTest is Cards.Menagerie.Barge || cardTest is Cards.Menagerie.BountyHunter || cardTest is Cards.Menagerie.Horse || cardTest is Cards.Menagerie.Mastermind || cardTest is Cards.Menagerie.Scrap || cardTest is Cards.Menagerie.VillageGreen || cardTest is Cards.Promotional.Church || cardTest is Cards.Promotional.Dismantle || cardTest is Cards.Promotional.Prince ) continue; // Score the card and then pick it if it's higher than the previous best card double baseScore = RealThis._Game.ComputeCost(cardTest).Coin.Value; if (cardTest is Cards.Base.Chapel) baseScore = 1; else if (cardTest is Cards.Prosperity.Bishop || cardTest is Cards.Prosperity2ndEdition.Bishop) baseScore = 3.5; else if (cardTest is Cards.Prosperity.Peddler) baseScore = 4.5; else if (cardTest is Cards.Cornucopia.BagOfGold || cardTest is Cards.Cornucopia2ndEdition.BagOfGold) baseScore = 6; else if (cardTest is Cards.Cornucopia.Followers || cardTest is Cards.Cornucopia2ndEdition.Followers) baseScore = 6; else if (cardTest is Cards.Cornucopia.Princess) baseScore = 6; else if (cardTest is Cards.Cornucopia.TrustySteed || cardTest is Cards.Cornucopia2ndEdition.TrustySteed) baseScore = 6; else if (cardTest is Cards.Hinterlands.SpiceMerchant || cardTest is Cards.Hinterlands2ndEdition.SpiceMerchant) baseScore = 2; else if (cardTest is Cards.DarkAges.Beggar || cardTest is Cards.DarkAges2ndEdition.Beggar) baseScore = 1; else if (cardTest is Cards.Guilds.Doctor || cardTest is Cards.Guilds2ndEdition.Doctor) baseScore = 1.2; if (baseScore > bestScore) { cardToSetAside = cardTest; bestScore = baseScore; } } switch (choice.ChoiceType) { case ChoiceType.Options: // "Do you want to set this card aside?" // Only do this if there's a useful card to set aside if (cardToSetAside != null) // Yes, set Prince aside return new ChoiceResult(new List { choice.Options[0].Text }); // No, don't set Prince aside return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceType.Cards: if (cardToSetAside != null && choice.Cards.Contains(cardToSetAside)) return new ChoiceResult(new CardCollection { cardToSetAside }); return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); default: return base.DecidePrince(choice); } } protected override ChoiceResult DecideProcession(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // Priority first is for Fortress, since it's soooo awesome to trash Fortress var bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Fortress); // Reserve cards are generally good to duplicate if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Reserve) && !(c is Cards.Adventures.Duplicate) && !(c is Cards.Adventures.Teacher) ); // Next priority is for Ruins, since they pretty much suck if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedVillage); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedMarket); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Survivors); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedLibrary); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.AbandonedMine); // We need to be a little careful with Procession if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(c => !(c is Cards.Base.Chapel) && !(c is Cards.Base.Library) && !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.SecretChamber) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Outpost) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.Tactician) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Outpost) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.Tactician) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.CountingHouse) && !(c is Cards.Prosperity.Forge) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity.Watchtower) && !(c is Cards.Prosperity2ndEdition.CountingHouse) && !(c is Cards.Prosperity2ndEdition.Forge) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Prosperity2ndEdition.Watchtower) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.JunkDealer) && !(c is Cards.DarkAges.Procession) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.DarkAges2019Errata.Procession) && !(c is Cards.Guilds.MerchantGuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.MerchantGuild) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Adventures.Champion) && !(c is Cards.Adventures.Teacher) && !(c is Cards.Adventures2ndEdition.Champion) && !(c is Cards.Empires.Catapult) && !(c is Cards.Empires.Sacrifice) && !(c is Cards.Empires.Temple) && !(c is Cards.Nocturne.CursedVillage) && !(c is Cards.Renaissance.Hideout) && !(c is Cards.Renaissance.Recruiter) && !(c is Cards.Renaissance.Research) && !(c is Cards.Menagerie.BountyHunter) && !(c is Cards.Menagerie.Scrap) && !(c is Cards.Promotional.Dismantle) && !(c.Category.HasFlag(Categories.Traveller)) && RealThis._Game.Table.TableEntities.Values.OfType().Any( s => s.CanGain() && s.TopCard.Category.HasFlag(Categories.Action) && s.CurrentCost == RealThis._Game.ComputeCost(c) + new Cost(1) ))); // ----------- ^^^ --- this is to make sure that we can actually gain a card from the card we're trashing // Let's trash some Ruins if we can't find anything fun if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins))); // Reserve cards that are definitely not detrimental to play twice if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Reserve)); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(c => !(c is Cards.Base.Remodel) && !(c is Cards.Intrigue.Upgrade) && !(c is Cards.Seaside.Island) && !(c is Cards.Seaside.Lookout) && !(c is Cards.Seaside.Salvager) && !(c is Cards.Seaside.TreasureMap) && !(c is Cards.Seaside2ndEdition.Island) && !(c is Cards.Seaside2ndEdition.Lookout) && !(c is Cards.Seaside2ndEdition.Salvager) && !(c is Cards.Seaside2ndEdition.TreasureMap) && !(c is Cards.Prosperity.TradeRoute) && !(c is Cards.Prosperity2ndEdition.TradeRoute) && !(c is Cards.Cornucopia.Remake) && !(c is Cards.Cornucopia2ndEdition.Remake) && !(c is Cards.Hinterlands.Develop) && !(c is Cards.Hinterlands2ndEdition.Develop) && !(c is Cards.DarkAges.Rats) && !(c is Cards.DarkAges.Rebuild) && !(c is Cards.DarkAges2ndEdition.Rats) && !(c is Cards.DarkAges2ndEdition.Rebuild) && !(c is Cards.Guilds.Stonemason) && !(c is Cards.Guilds2ndEdition.Stonemason) && !(c is Cards.Empires.Catapult) && !(c is Cards.Empires.Sacrifice) && !(c is Cards.Empires.Temple) && !(c is Cards.Renaissance.Hideout) && !(c is Cards.Renaissance.Recruiter) && !(c is Cards.Renaissance.Research) && !(c is Cards.Menagerie.BountyHunter) && !(c is Cards.Menagerie.Scrap) && !(c is Cards.Promotional.Dismantle) && RealThis._Game.Table.TableEntities.Values.OfType().Any( s => s.CanGain() && s.TopCard.Category.HasFlag(Categories.Action) && s.CurrentCost == RealThis._Game.ComputeCost(c) + new Cost(1) ))); // ^^^ --- this is to make sure that we can actually gain a card from the card we're trashing if (bestCard != null) return new ChoiceResult(new CardCollection { bestCard }); // Don't play anything return new ChoiceResult(new CardCollection()); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } return base.DecideProcession(choice); } protected override ChoiceResult DecideQuest(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: Option option; if (RealThis.Hand[Cards.Universal.TypeClass.Curse].Count >= 2) option = choice.Options.SingleOrDefault(o => o.Text == "2 Curses"); else if (RealThis.Hand[Categories.Attack].Count >= 1) option = choice.Options.SingleOrDefault(o => o.Text == "An Attack"); else option = choice.Options.SingleOrDefault(o => o.Text == "6 cards"); if (option != null) return new ChoiceResult(new List { option.Text }); break; case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Minimum))); } return base.DecideQuest(choice); } protected override ChoiceResult DecideRabble(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cards = new CardCollection(choice.Cards); // Order them in roughly random order cards.Shuffle(); return new ChoiceResult(cards); } protected override ChoiceResult DecideRaider(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Minimum))); } protected override ChoiceResult DecideRats(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideRatcatcher(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideRaze(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == "You may trash a card from your hand (otherwise, trash this)") { var bestCardToTrash = FindBestCardsToTrash(choice.Cards, 1).FirstOrDefault(); if (bestCardToTrash == null || ComputeValueInDeck(bestCardToTrash) > ComputeValueInDeck((Card)choice.Source)) return new ChoiceResult(new CardCollection()); return new ChoiceResult(new CardCollection { bestCardToTrash }); } if (choice.Text == "Choose a card to put into your hand") { return new ChoiceResult(new CardCollection { choice.Cards.OrderByDescending(ComputeValueInDeck).First() }); } return base.DecideRaze(choice); } protected override ChoiceResult DecideRebuild(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.SuppliesAndCards: var colonyExists = false; var colonyAvailable = false; var colonyCount = 0; if (RealThis._Game.Table.TableEntities.ContainsKey(Cards.Prosperity.TypeClass.Colony)) { colonyExists = true; colonyAvailable = ((ISupply)RealThis._Game.Table.TableEntities[Cards.Prosperity.TypeClass.Colony]).CanGain(); colonyCount = RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Colony, onlyCurrentlyDrawable: true); } var provinceAvailable = RealThis._Game.Table.Province.CanGain(); var provinceCount = RealThis.CountAll(RealThis, c => c is Cards.Universal.Province, onlyCurrentlyDrawable: true); var duchyAvailable = RealThis._Game.Table.Province.CanGain(); var duchyCount = RealThis.CountAll(RealThis, c => c is Cards.Universal.Duchy, onlyCurrentlyDrawable: true); var estateAvailable = RealThis._Game.Table.Province.CanGain(); var estateCount = RealThis.CountAll(RealThis, c => c is Cards.Universal.Estate, onlyCurrentlyDrawable: true); ISupply victorySupply = null; if (colonyExists && (colonyCount > 0 || colonyAvailable)) victorySupply = choice.Supplies.Select(kvp => kvp.Value).OfType().First(s => s.Type == Cards.Prosperity.TypeClass.Colony); if (victorySupply == null && provinceCount > 0) victorySupply = choice.Supplies.Select(kvp => kvp.Value).OfType().First(s => s.Type == Cards.Universal.TypeClass.Province); if (victorySupply == null) { victorySupply = choice.Supplies.Select(kvp => kvp.Value).OfType().Where( s => s.TopCard.Category.HasFlag(Categories.Victory) && s.Type != Cards.Hinterlands.TypeClass.Farmland && s.Type != Cards.Hinterlands2ndEdition.TypeClass.Farmland) .OrderByDescending(s => s.BaseCost).First(); } return new ChoiceResult(victorySupply); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideRebuild(choice); } } protected override ChoiceResult DecideRecruiter(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideRemake(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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.OfType(), null, false)); default: return base.DecideRemake(choice); } } protected override ChoiceResult DecideRemodel(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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.OfType(), null, false)); default: return base.DecideRemodel(choice); } } protected override ChoiceResult DecideReplace(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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.OfType(), null, false)); } return base.DecideReplace(choice); } protected override ChoiceResult DecideResearch(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideRitual(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Priority first is for Fortress, since it's soooo awesome to trash Fortress var bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Fortress); // Let's get rid of cards we don't want to gain if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Rats); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => ShouldBuy(c.Type) && c.BaseCost.Coin > 0); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains); if (bestCard == null && GameProgressLeft < 0.4) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.BorderVillage || c is Cards.Hinterlands2ndEdition.BorderVillage); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.Farmland || c is Cards.Hinterlands2ndEdition.Farmland); // Second priority is for Ruins, since they pretty much suck if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedVillage); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedMarket); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Survivors); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.RuinedLibrary); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.AbandonedMine); //// Base VP cards are pretty good for trashing, if nothing else //if (bestCard == null) // bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Province); //if (bestCard == null && RealThis.CountAll(predicate: c => c is Cards.Intrigue.Duke, onlyObtainable: false) == 0) // bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Duchy); //if (bestCard == null) // bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Estate); // Shelters are at least break-even if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Hovel); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Necropolis); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate); if (bestCard == null) bestCard = FindBestCardsToTrash(choice.Cards, 1).FirstOrDefault(); return new ChoiceResult(new CardCollection { bestCard }); } protected override ChoiceResult DecideRogue(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == "Choose a card to gain from the trash") return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); if (choice.Text == Resource.ChooseACardToTrash) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); return base.DecideRogue(choice); } protected override ChoiceResult DecideSaboteur(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var bestSupply = FindBestCardForCost(choice.Supplies.Values.OfType(), null, false); if (RealThis.Hand[Cards.Hinterlands.TypeClass.Trader].Any() && RealThis._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.Type == Cards.Universal.TypeClass.Curse) bestSupply = null; // Never, ever gain a Ruins else if (bestSupply.Type == Cards.DarkAges.TypeClass.RuinsSupply) bestSupply = null; else if (bestSupply.Type == Cards.Universal.TypeClass.Copper) { // Only ever gain a Copper in specific situations (Counting House, Coppersmith, Gardens, etc.) var copperUsingCards = RealThis.CountAll(RealThis, c => c is Cards.Base.Gardens || c is Cards.Base.Moneylender || c is Cards.Base2ndEdition.Moneylender || c is Cards.Intrigue.Coppersmith || c is Cards.Alchemy.Apothecary || c is Cards.Alchemy2ndEdition.Apothecary || c is Cards.Prosperity.CountingHouse || c is Cards.Prosperity2ndEdition.CountingHouse); var copperCards = RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper); var treasureCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure)); var totalCards = RealThis.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 DecideSacredGrove(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Only if we've got a Curse or Ruins in hand if (choice.Triggers.Any(t => t is Cards.Nocturne.TheFlamesGift) && RealThis.Hand.Any(c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new List { choice.Options[0].Text }); // Always except for the discard ones? if (choice.Triggers.Any(t => t is Cards.Nocturne.TheMoonsGift || t is Cards.Nocturne.TheMountainsGift || t is Cards.Nocturne.TheRiversGift || t is Cards.Nocturne.TheSeasGift || t is Cards.Nocturne.TheSunsGift || t is Cards.Nocturne.TheSwampsGift || t is Cards.Nocturne.TheWindsGift)) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideSacrifice(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideSaltTheEarth(Choice choice) { // Salt the Earth's decision tree is pretty complex and should require a bit more thought than "random" return base.DecideSaltTheEarth(choice); } protected override ChoiceResult DecideSalvager(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideSanctuary(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Sanctuary is a little fancier since we can Exile Victory cards without much penalty // (and generally prefer to Exile them anyway) var bestVictory = choice.Cards.Where(c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && !c.Category.HasFlag(Categories.Night) ).OrderByDescending(c => ComputeValueInDeck(c)).FirstOrDefault(); if (bestVictory != null) return new ChoiceResult(bestVictory); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1, true))); } protected override ChoiceResult DecideSauna(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // Always return "Yes" return new ChoiceResult(new List { choice.Options[0].Text }); // Yes case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1, true))); } return base.DecideSauna(choice); } protected override ChoiceResult DecideSave(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // This isn't very good. Save offers some pretty complicated decision-making choices that need to be handled better return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideScavenger(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // I have no freaking clue... -- just choose at random return new ChoiceResult(new List { choice.Options[_Game.RNG.Next(0, 1)].Text }); case ChoiceType.Cards: // Take the best card (? I'unno... seems OK-ish) return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); default: return base.DecideScavenger(choice); } } protected override ChoiceResult DecideScrap(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // This is a lot of logic for a card we don't want in our deck! switch (choice.ChoiceType) { case ChoiceType.Cards: // 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 Card scrapBestCard = null; // Useless Seahags & Familiars go first if (_Game.Table.Curse.Count < _Game.Players.Count) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar); // Rats are usually a great choice as well if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Rats); // Curses go next if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Curse)); // Ruins go next if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Ruins)); // Estates are usually a great choice as well if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Estate); // Overgrown Estates are a good choice as well if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate); // Fortress is sweet -- it comes right back into my hand if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Fortress); // Ill-Gotten Gains is nice if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains); // Masterpiece is nice if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); // Hovels aren't horrible to trash if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.DarkAges.Hovel); // 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 (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => (c is Cards.Base.Bureaucrat && GameProgressLeft < 0.4) || (c is Cards.Base.Moneylender && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || c is Cards.Base.Remodel || (c is Cards.Base2ndEdition.Bureaucrat && GameProgressLeft < 0.4) || (c is Cards.Base2ndEdition.Moneylender && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || (c is Cards.Intrigue.Coppersmith && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 5) || (c is Cards.Intrigue.Ironworks && GameProgressLeft < 0.4) || c is Cards.Intrigue.Masquerade || c is Cards.Intrigue.TradingPost || c is Cards.Intrigue.Upgrade || (c is Cards.Intrigue2ndEdition.Ironworks && GameProgressLeft < 0.4) || c is Cards.Intrigue2ndEdition.Masquerade || c is Cards.Seaside.Ambassador || c is Cards.Seaside.Lookout || c is Cards.Seaside.Salvager || (c is Cards.Seaside.TreasureMap && RealThis.CountAll(RealThis, cG => cG is Cards.Universal.Gold) > 2) || c is Cards.Seaside2ndEdition.Ambassador || c is Cards.Seaside2ndEdition.Lookout || c is Cards.Seaside2ndEdition.Salvager || (c is Cards.Seaside2ndEdition.TreasureMap && RealThis.CountAll(RealThis, cG => cG is Cards.Universal.Gold) > 2) || (c is Cards.Alchemy.Potion && !_Game.Table.TableEntities.Values.OfType().Any(s => s.BaseCost.Potion.Value > 0 && s.CanGain())) || (c is Cards.Prosperity.CountingHouse && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 5) || c is Cards.Prosperity.Expand || c is Cards.Prosperity.Forge || (c is Cards.Prosperity.Loan && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || (c is Cards.Prosperity2ndEdition.CountingHouse && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 5) || c is Cards.Prosperity2ndEdition.Expand || c is Cards.Prosperity2ndEdition.Forge || (c is Cards.Prosperity2ndEdition.Loan && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper) < 3) || c is Cards.Cornucopia.Remake || c is Cards.Cornucopia2ndEdition.Remake || c is Cards.Hinterlands.Develop || (c is Cards.Hinterlands.SpiceMerchant && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper || cC is Cards.Prosperity.Loan || cC is Cards.Prosperity2ndEdition.Loan) < 4) || c is Cards.Hinterlands2ndEdition.Develop || (c is Cards.Hinterlands2ndEdition.SpiceMerchant && RealThis.CountAll(RealThis, cC => cC is Cards.Universal.Copper || cC is Cards.Prosperity.Loan || cC is Cards.Prosperity2ndEdition.Loan) < 4) || (c is Cards.DarkAges.HuntingGrounds && GameProgressLeft < 0.4) || (c is Cards.DarkAges.SirVander && GameProgressLeft < 0.3) || (c is Cards.DarkAges.Armory && GameProgressLeft < 0.4) || (c is Cards.DarkAges2ndEdition.SirVander && GameProgressLeft < 0.3) || (c is Cards.DarkAges2ndEdition.Armory && GameProgressLeft < 0.4) || c is Cards.Guilds.Stonemason || c is Cards.Guilds2ndEdition.Stonemason || c is Cards.Menagerie.Scrap ); // Copper is a distant 10th if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); // Masterpiece's main benefit is its on-buy ability, so might as well trash it now // Same goes for Ill-Gotten Gain's on-gain ability if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece || c is Cards.Hinterlands.IllGottenGains || c is Cards.Hinterlands2ndEdition.IllGottenGains); // If a suitable one's STILL not been found, allow Peddler, well, because getting an extra 4 VPs off Peddler is *AMAZING* if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Peddler); // Otherwise, choose a non-Victory card to trash if (scrapBestCard == null) { var scrCards = FindBestCardsToTrash(choice.Cards.Where(c => !c.Category.HasFlag(Categories.Victory)), 1).ToList(); if (scrCards.Any()) scrapBestCard = scrCards.ElementAt(0); } // Duchies or Dukes are usually an OK choice if (scrapBestCard == null) scrapBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Duchy || c is Cards.Intrigue.Duke ); // OK, last chance... just PICK one! if (scrapBestCard == null) { scrapBestCard = FindBestCardsToTrash(choice.Cards, 1).FirstOrDefault(); } if (scrapBestCard != null) return new ChoiceResult(new CardCollection { scrapBestCard }); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); case ChoiceType.Options: // Priority list (the ones chosen is selected later) var choices = new List { choice.Options[3].Text, // +1 Coin choice.Options[0].Text, // +1 Card choice.Options[5].Text, // Gain Horse choice.Options[4].Text, // Gain Silver choice.Options[2].Text, // +1 Buy choice.Options[1].Text, // +1 Action }; if (RealThis.Hand.Count(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c)) > ActionsAvailable()) choices.Insert(1, choice.Options[1].Text); // +1 Action return new ChoiceResult(choices.Take(choice.Minimum)); } return base.DecideScrap(choice); } protected override ChoiceResult DecideScheme(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always take the most expensive card return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, choice.Maximum))); } protected override ChoiceResult DecideScepter(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var availableActions = RealThis.InPlayAndSetAside[c => c.Category.HasFlag(Categories.Action) && RealThis.CurrentTurn.CardsPlayed.Contains(c) ]; var bestCards = FindBestCardsToPlay(availableActions); if (bestCards.Any()) return new ChoiceResult(new OptionCollection { choice.Options[1] }); return new ChoiceResult(new OptionCollection { choice.Options[0] }); case ChoiceType.Cards: // Always take the most expensive card return new ChoiceResult(new ItemCollection { FindBestCardToPlay(choice.Cards) }); } return base.DecideScepter(choice); } protected override ChoiceResult DecideScout(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var scoutCards = new CardCollection(choice.Cards); // Order them in roughly random order scoutCards.Shuffle(); return new ChoiceResult(scoutCards); } protected override ChoiceResult DecideScoutingParty(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == "Choose 3 cards to discard") { return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 3))); } // Put them back in a random order return base.DecideScoutingParty(choice); } protected override ChoiceResult DecideSculptor(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideScryingPool(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.PlayerSource == RealThis) { if (IsCardOkForMeToDiscard(choice.Triggers.OfType().First()) && !choice.Triggers[0].Category.HasFlag(Categories.Ruins)) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } if (!IsCardOkForMeToDiscard(choice.Triggers.OfType().First())) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideSeaway(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var maxCards = 0; var maxCardsSupplies = new List(); var supplies = choice.Supplies.Values.OfType(); foreach (var supply in supplies) { var count = RealThis.CountAll(RealThis, c => supply.Types.Contains(c.Type)); if (count > maxCards) maxCards = count; if (count == maxCards) maxCardsSupplies.Add(supply); } return new ChoiceResult(FindBestCardForCost(maxCardsSupplies.Any() ? maxCardsSupplies : supplies, null, false)); } protected override ChoiceResult DecideSecretCave(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cards = choice.ChoiceOutcome == ChoiceOutcome.Select ? RealThis.Hand : choice.Cards; var secretCaveCards = new List(); // TODO -- the AI makes slightly bad decisions when it comes to Cellar -- Fix me! secretCaveCards.AddRange(cards.Where(c => c is Cards.Hinterlands.Tunnel || c is Cards.Hinterlands2ndEdition.Tunnel)); secretCaveCards.AddRange(cards.Where(c => c is Cards.Universal.Curse)); secretCaveCards.AddRange(cards.Where(c => c.Category.HasFlag(Categories.Ruins))); secretCaveCards.AddRange(cards.Where(c => c is Cards.DarkAges.Hovel)); secretCaveCards.AddRange(cards.Where(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate)); secretCaveCards.AddRange(cards.Where(c => c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Rats)); secretCaveCards.AddRange(cards.Where(c => (c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && c.Type != Cards.Universal.TypeClass.Estate && c.Type != Cards.Universal.TypeClass.Province))); if (cards.Any(c => c is Cards.Universal.Estate)) secretCaveCards.AddRange(cards.Where(c => c is Cards.Universal.Estate).Take(cards.Count(c => c is Cards.Universal.Estate) - cards.Count(c => c is Cards.Intrigue.Baron))); if (cards.Any(c => c is Cards.Universal.Province)) secretCaveCards.AddRange(cards.Where(c => c is Cards.Universal.Province).Take(cards.Count(c => c is Cards.Universal.Province) - cards.Count(c => c is Cards.Cornucopia.Tournament || c is Cards.Cornucopia2ndEdition.Tournament))); if (GameProgressLeft < 0.63) secretCaveCards.AddRange(cards.Where(c => c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece)); secretCaveCards = secretCaveCards.Distinct().ToList(); switch (choice.ChoiceOutcome) { case ChoiceOutcome.Select: if (secretCaveCards.Count >= 3) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceOutcome.Discard: return new ChoiceResult(new CardCollection(secretCaveCards.Take(3))); } return base.DecideSecretCave(choice); } protected override ChoiceResult DecideSecretChamber(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.ChoiceOutcome == ChoiceOutcome.Select) { // Order all the cards var cardsToReturn = FindBestCardsToDiscard(choice.Cards, choice.Cards.Count()).ToList(); // Try to save 1 Curse if we can if (choice.Triggers[0].Type == Cards.Prosperity.TypeClass.Mountebank || choice.Triggers[0].Type == Cards.Prosperity2ndEdition.TypeClass.Mountebank) { cardsToReturn = cardsToReturn.Take(3).ToList(); if (cardsToReturn[0] is Cards.Universal.Curse) return new ChoiceResult(new CardCollection(cardsToReturn.Skip(1))); if (cardsToReturn[1] is Cards.Universal.Curse) return new ChoiceResult(new CardCollection { cardsToReturn[0], cardsToReturn[2] }); } // Try to not put Treasure cards onto our deck, even if that means putting Action cards there else if (choice.Triggers[0].Type == Cards.Seaside.TypeClass.PirateShip || choice.Triggers[0].Type == Cards.Seaside2ndEdition.TypeClass.PirateShip) { var pirateShipCards = new CardCollection(cardsToReturn.Where(c => !c.Category.HasFlag(Categories.Treasure))); if (pirateShipCards.Count < 2) pirateShipCards.AddRange(cardsToReturn.Where(c => !c.Category.HasFlag(Categories.Treasure)).Take(2 - pirateShipCards.Count)); return new ChoiceResult(pirateShipCards); } return new ChoiceResult(new CardCollection(cardsToReturn.Take(2))); } var scCards = new CardCollection(); foreach (var card in choice.Cards) { if (card.Category.HasFlag(Categories.Curse) || (card.Category.HasFlag(Categories.Victory) && !card.Category.HasFlag(Categories.Treasure)) || (card is Cards.Universal.Copper && RealThis.InPlay[Cards.Intrigue.TypeClass.Coppersmith].Count == 0) || (ActionsAvailable() == 0 && !card.Category.HasFlag(Categories.Treasure))) scCards.Add(card); } return new ChoiceResult(scCards); } protected override ChoiceResult DecideSecretPassage(Choice choice) { return base.DecideSecretPassage(choice); } protected override ChoiceResult DecideSeer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection { choice.Cards.OrderByDescending(ComputeValueInDeck).First() }); } protected override ChoiceResult DecideSentry(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.ChoiceOutcome == ChoiceOutcome.Trash) { var toTrash = FindBestCardsToTrash(choice.Cards, choice.Maximum, true); return new ChoiceResult(new CardCollection(toTrash.Take(2))); } else if (choice.ChoiceOutcome == ChoiceOutcome.Discard) { var averageDrawPileValue = RealThis.DrawPile.Count == 0 ? 0 : RealThis.DrawPile.LookThrough(c => true).Sum(c => ComputeValueInDeck(c)) / RealThis.DrawPile.Count; var toDiscard = FindBestCardsToDiscard(choice.Cards, choice.Maximum) .TakeWhile( c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && ComputeValueInDeck(c) > 0.85 * averageDrawPileValue ).Take(2); return new ChoiceResult(new CardCollection(toDiscard)); } var sentryCards = new CardCollection(choice.Cards); // Order them in roughly random order sentryCards.Shuffle(); return new ChoiceResult(sentryCards); } protected override ChoiceResult DecideSettlers(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always Yes return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideSewers(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1, true))); } protected override ChoiceResult DecideShepherd(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Maybe there are other situational cards we'd want to keep around (e.g. Province w/ Tournament) -- need to investigate more var toDiscard = choice.Cards.Where(c => !( c is Cards.Intrigue.Harem || c is Cards.Intrigue.Nobles || c is Cards.Intrigue2ndEdition.Mill || c is Cards.Intrigue2ndEdition.Nobles || c is Cards.Seaside.Island || c is Cards.Seaside2ndEdition.Island || c is Cards.DarkAges.DameJosephine || c is Cards.DarkAges2ndEdition.DameJosephine || c is Cards.Adventures.DistantLands )); return new ChoiceResult(new CardCollection(toDiscard)); } protected override ChoiceResult DecideSilos(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always discard all of the coppers return new ChoiceResult(new ItemCollection(choice.Cards)); } protected override ChoiceResult DecideSinisterPlot(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var token = _Game.Table[Cards.Renaissance.TypeClass.SinisterPlot].Tokens.OfType().FirstOrDefault(spm => spm.Owner == RealThis); if (token != null) { // Only draw cards if we have 4+ tokens on the pile if (token.TokenCount >= 4) return new ChoiceResult(new List { choice.Options[1].Text }); return new ChoiceResult(new List { choice.Options[0].Text }); } return base.DecideSinisterPlot(choice); } protected override ChoiceResult DecideSirBailey(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideSirDestry(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideSirMartin(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideSirMichael(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.ChoiceOutcome == ChoiceOutcome.Discard) return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 3))); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideSirVander(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideSmallCastle(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Trash SmallCastle if there are no castles to gain var castleSupply = (ISupply)_Game.Table.TableEntities[Cards.Empires.TypeClass.Castles]; var cardToTrash = castleSupply.TopCard == null ? null : choice.Cards.OrderBy(c => c.BaseCost.Coin.Value).FirstOrDefault(); // Trash HumbleCastle, CrumblingCastle, or Haunted Castle (but only if there's a more expensive Castle to gain) instead of SmallCastle // Otherwise, trash SmallCastle instead if (cardToTrash != null && !(cardToTrash is Cards.Empires.HumbleCastle || cardToTrash is Cards.Empires.CrumblingCastle || cardToTrash is Cards.Empires.HauntedCastle && castleSupply.TopCard.BaseCost.Coin.Value > cardToTrash.BaseCost.Coin.Value)) cardToTrash = null; if (cardToTrash != null) return new ChoiceResult(new CardCollection { cardToTrash }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideSmugglers(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); } protected override ChoiceResult DecideSoldier(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideSpiceMerchant(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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].Text }); case ChoiceType.Cards: // Only ever trash Coppers var smCopper = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); if (smCopper != null) return new ChoiceResult(new CardCollection { smCopper }); return new ChoiceResult(new CardCollection()); default: return base.DecideSpiceMerchant(choice); } } protected override ChoiceResult DecideSprawlingCastle(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var scChoice = choice.Options[0].Text; // 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 (RealThis._Game.Table.Duchy.Count == 0) scChoice = choice.Options[1].Text; if (RealThis._Game.IsEndgameTriggered && RealThis._Game.Table.Estate.Count >= 3 && RealThis.CountAll(RealThis, c => c is Cards.Base.Gardens || c is Cards.Hinterlands.SilkRoad || c is Cards.Hinterlands2ndEdition.SilkRoad) > 0) scChoice = choice.Options[1].Text; return new ChoiceResult(new List { scChoice }); } protected override ChoiceResult DecideSpy(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.PlayerSource == RealThis) { if (IsCardOkForMeToDiscard(choice.Triggers.OfType().First())) return new ChoiceResult(new List { choice.Options[0].Text }); // Discard return new ChoiceResult(new List { choice.Options[1].Text }); // Put back } if (!IsCardOkForMeToDiscard(choice.Triggers.OfType().First())) return new ChoiceResult(new List { choice.Options[0].Text }); // Discard return new ChoiceResult(new List { choice.Options[1].Text }); // Put back } protected override ChoiceResult DecideSquire(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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 var squireChoices = new List { choice.Options[2].Text }; if (RealThis.Hand[Categories.Action].Count >= 2) squireChoices[0] = choice.Options[0].Text; return new ChoiceResult(squireChoices); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideSquire(choice); } } protected override ChoiceResult DecideStables(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always discard Copper var stablesBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); if (stablesBestCard == null && (RealThis.Hand[Cards.Alchemy.TypeClass.Potion].Count > 1 || !_Game.Table.TableEntities.Values.OfType().Any(s => s.BaseCost.Potion > 0 && s.CanGain()))) stablesBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Alchemy.Potion); if (stablesBestCard == null && RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper) < 4) stablesBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan); if (stablesBestCard == null && RealThis.Hand[c => c is Cards.Hinterlands.FoolsGold || c is Cards.Hinterlands2ndEdition.FoolsGold].Count == 1) stablesBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Hinterlands.FoolsGold || c is Cards.Hinterlands2ndEdition.FoolsGold); if (stablesBestCard == null) stablesBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); if (stablesBestCard != null) return new ChoiceResult(new CardCollection { stablesBestCard }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideStarChart(Choice choice) { // For now, always put the best card on top of the shuffling pile // This is very bad for instances involving Thief or Saboteur, but for now, it'll function return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); } protected override ChoiceResult DecideStash(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // For now, always put Stash on top of the shuffling pile // This is very bad for instances involving Thief or Saboteur, but for now, it'll function var cards = new CardCollection(); cards.AddRange(choice.Cards.Where(c => c is Cards.Promotional.Stash || c is Cards.Promotional2ndEdition.Stash)); cards.AddRange(choice.Cards.Where(c => !(c is Cards.Promotional.Stash || c is Cards.Promotional2ndEdition.Stash))); return new ChoiceResult(cards); } protected override ChoiceResult DecideSteward(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // Trash 2 cards if we have 2 Curse/Ruins cards if (RealThis.Hand[Categories.Curse].Count + RealThis.Hand[Categories.Ruins].Count >= 2) return new ChoiceResult(new List { choice.Options[2].Text }); // Otherwise, take 2 Coins if we have at least 3 already if (RealThis.Currency.Coin >= 3) return new ChoiceResult(new List { choice.Options[1].Text }); // Otherwise, just draw 2 cards return new ChoiceResult(new List { choice.Options[0].Text }); case ChoiceType.Cards: // Trashing cards if (choice.Cards.Count(c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins)) >= 2) return new ChoiceResult(new CardCollection(choice.Cards.Where(c => c is Cards.Universal.Curse || c.Category.HasFlag(Categories.Ruins)).Take(2))); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 2))); default: return base.DecideSteward(choice); } } protected override ChoiceResult DecideStonemason(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: for (var index = choice.Options.Count - 1; index >= 0; index--) { // Overpay by as much as we can properly gain Action cards var overpayAmount = new Currency(choice.Options[index].Text); if (_Game.Table.TableEntities.Values.OfType().Any(s => s.CanGain() && s.TopCard.Category.HasFlag(Categories.Action) && s.CurrentCost == overpayAmount)) return new ChoiceResult(new List { choice.Options[index].Text }); } return base.DecideStonemason(choice); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceType.Supplies: if (choice.Text.StartsWith("Gain a card costing less than", StringComparison.InvariantCulture)) return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideStonemason(choice); } } protected override ChoiceResult DecideStoreroom(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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 (ActionsAvailable() == 0) // Discard all non-Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => !c.Category.HasFlag(Categories.Treasure)))); // Discard all non-Action/Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure)))); } // Cards for coins (Secret Chamber/Vault) // "+1" if (ActionsAvailable() == 0) // Discard all non-Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => !c.Category.HasFlag(Categories.Treasure)))); // Discard all non-Action/Treasure cards return new ChoiceResult(new CardCollection(choice.Cards.Where(c => !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure)))); } protected override ChoiceResult DecideStoryteller(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always play Coppers (I think?) & Loan (Yes?) & Silvers 50% of the time var storytellerCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); if (storytellerCard == null) storytellerCard = choice.Cards.FirstOrDefault(c => c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan); if (storytellerCard == null && _Game.RNG.Next(0, 1) >= 1) storytellerCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Silver); if (storytellerCard == null) return new ChoiceResult(new CardCollection()); return new ChoiceResult(new CardCollection { storytellerCard }); } protected override ChoiceResult DecideSummon(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideSurvivors(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var totalDeckDiscardability = RealThis.SumAll(RealThis, c => true, c => c is Card ? ComputeDiscardValue((Card)c) : 0, onlyCurrentlyDrawable: true); var totalCards = RealThis.CountAll(RealThis, c => true, onlyCurrentlyDrawable: true); var cardsDiscardability = choice.Triggers.OfType().Sum(c => ComputeDiscardValue(c)); // If it's better to keep these cards than discard them if (cardsDiscardability / choice.Triggers.Count >= totalDeckDiscardability / totalCards) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceType.Cards: var oracleCards = new CardCollection(choice.Cards); // Order them in roughly random order oracleCards.Shuffle(); return new ChoiceResult(oracleCards); } return base.DecideSurvivors(choice); } protected override ChoiceResult DecideSwindler(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindWorstCardForCost(choice.Supplies.Values.OfType(), null)); } protected override ChoiceResult DecideTax(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var taxableSupplies = new List(); foreach (var supply in choice.Supplies.Values.OfType().Where(s => s.TableableType != Cards.Universal.TypeClass.Curse)) { // Don't Tax Embargoed piles if there are Curses left if (supply.CanGain() && !supply.Tokens.Any(t => t is Cards.Seaside.EmbargoToken)) taxableSupplies.Add(supply); } if (taxableSupplies.Count == 0) taxableSupplies.Add((ISupply)choice.Supplies[Cards.Universal.TypeClass.Province]); return new ChoiceResult(taxableSupplies[_Game.RNG.Next(taxableSupplies.Count)]); } protected override ChoiceResult DecideTaxman(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Cards: // For Taxman, we have a slightly different priority than with Mine // This could be a bit better (taking into account the other players' hands), but for now, it will suffice Card mineCard = mineCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); if (mineCard == null) mineCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Silver); if (mineCard == null && _Game.Table.TableEntities.ContainsKey(Cards.Prosperity.TypeClass.Platinum) && ((ISupply)_Game.Table.TableEntities[Cards.Prosperity.TypeClass.Platinum]).CanGain()) mineCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Gold); if (mineCard == null) mineCard = choice.Cards.FirstOrDefault(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); if (mineCard == null) // Pick a random Treasure at this point mineCard = choice.Cards.ElementAt(_Game.RNG.Next(choice.Cards.Count())); return new ChoiceResult(new CardCollection { mineCard }); case ChoiceType.Supplies: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); default: return base.DecideTaxman(choice); } } protected override ChoiceResult DecideTeacher(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // TODO -- Implement logic for Teacher decisions. Ordering should be: // 1. Unplaced +1 Card // 2. Unplaced +1 Action if lots of Terminal actions // 3. Unplaced +1 Coin // 4. Unplaced +1 Action // 5. Unplaced +1 Buy // 6. ???? // Priority should be for Action piles that we have in hand already // Secondary priority (possibly overriding) is how many of particular Action cards we have in our deck List FindBestSupply(IEnumerable supplies) { var maxCards = 0; var maxCardsSupplies = new List(); foreach (var supply in supplies) { var count = RealThis.CountAll(RealThis, c => supply.Types.Contains(c.Type)); if (count > maxCards) { maxCards = count; maxCardsSupplies.Clear(); } if (maxCards > 0 && count == maxCards) maxCardsSupplies.Add(supply); } return maxCardsSupplies; } switch (choice.ChoiceType) { case ChoiceType.Options: ITableable existingLocation = null; ITableable FindExisting(Type tokenType) { return _Game.Table.TableEntities.FirstOrDefault(skv => skv.Value.Tokens.Any( t => t.GetType() == tokenType && ((PlayerToken)t).Owner == RealThis )).Value; } bool HasAnyTokens(ISupply supply) { return supply.Tokens.Any(t => t.Type == Cards.Adventures.TypeClass.PlusOneCardToken || t.Type == Cards.Adventures.TypeClass.PlusOneActionToken || t.Type == Cards.Adventures.TypeClass.PlusOneBuyToken || t.Type == Cards.Adventures.TypeClass.PlusOneCoinToken ); } var bestSuppliesTest = FindBestSupply(_Game.Table.TableEntities.OfType().Where(s => s.Category.HasFlag(Categories.Action))); var bestSupply = FindBestCardForCost(bestSuppliesTest, null, false); if (bestSupply != null && bestSupply.TopCard.Traits.HasFlag(Traits.Terminal) && !HasAnyTokens(bestSupply)) { existingLocation = FindExisting(Cards.Adventures.TypeClass.PlusOneActionToken); if (existingLocation == null) return new ChoiceResult(new List { choice.Options[1].Text }); } existingLocation = FindExisting(Cards.Adventures.TypeClass.PlusOneCardToken); if (existingLocation == null) return new ChoiceResult(new List { choice.Options[0].Text }); existingLocation = FindExisting(Cards.Adventures.TypeClass.PlusOneCoinToken); if (existingLocation == null) return new ChoiceResult(new List { choice.Options[3].Text }); existingLocation = FindExisting(Cards.Adventures.TypeClass.PlusOneActionToken); if (existingLocation == null) return new ChoiceResult(new List { choice.Options[1].Text }); existingLocation = FindExisting(Cards.Adventures.TypeClass.PlusOneBuyToken); if (existingLocation == null) return new ChoiceResult(new List { choice.Options[2].Text }); break; case ChoiceType.Supplies: // Implementing 2nd priority var supplies = choice.Supplies.Values.OfType(); var bestSupplies = FindBestSupply(supplies); return new ChoiceResult(FindBestCardForCost(bestSupplies.Any() ? bestSupplies : supplies, null, false)); } return base.DecideTeacher(choice); } protected override ChoiceResult DecideTemple(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var trashingCards = FindBestCardsToTrash(choice.Cards, 3) .Where(card => card.Category.HasFlag(Categories.Curse) || card.Category.HasFlag(Categories.Ruins) || card is Cards.Universal.Copper || card is Cards.Seaside.SeaHag || card is Cards.Seaside2ndEdition.SeaHag ); return new ChoiceResult(new CardCollection(trashingCards)); } protected override ChoiceResult DecideTheEarthsGift(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceOutcome) { case ChoiceOutcome.Discard: var toDiscard = choice.Cards.Where(c => c is Cards.Universal.Copper || c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); if (toDiscard.Any()) return new ChoiceResult(new CardCollection { toDiscard.First() }); return new ChoiceResult(new CardCollection()); case ChoiceOutcome.Gain: return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } return base.DecideTheEarthsGift(choice); } protected override ChoiceResult DecideTheFlamesGift(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var toTrash = new CardCollection(); // Always choose to trash a Curse if possible if (choice.Cards.Any(c => c is Cards.Universal.Curse)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Curse) }); // Always choose to trash a Ruins if possible if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c.Category.HasFlag(Categories.Ruins)) }); // Always choose to trash Hovel if possible if (choice.Cards.Any(c => c is Cards.DarkAges.Hovel)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.DarkAges.Hovel) }); // If there are no Curses left, choose to trash Sea Hag or Familiar if possible if (_Game.Table.Curse.Count == 0 && choice.Cards.Any(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar) }); // Always choose to trash Copper if we have at least 2 Golds or at least 5 Silvers if ((RealThis.CountAll(RealThis, c => c is Cards.Universal.Gold) >= 2 || RealThis.CountAll(RealThis, c => c is Cards.Universal.Silver) >= 5) && choice.Cards.Any(c => c is Cards.Universal.Copper)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Copper) }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideTheMoonsGift(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Maybe not always put the "best" card back if it's not good if (RealThis.DiscardPile.LookThrough(hC => hC.Category.HasFlag(Categories.Action) || hC.Category.HasFlag(Categories.Treasure) || hC.Category.HasFlag(Categories.Night)) .Select(hC => ComputeValueInDeck(hC)).Any(v => v >= 4.0)) return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideTheSkysGift(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cards = choice.ChoiceOutcome == ChoiceOutcome.Select ? RealThis.Hand : choice.Cards; var discardCards = new List(); // TODO -- the AI makes slightly bad decisions when it comes to Cellar -- Fix me! discardCards.AddRange(cards.Where(c => c is Cards.Hinterlands.Tunnel || c is Cards.Hinterlands2ndEdition.Tunnel)); discardCards.AddRange(cards.Where(c => c is Cards.Universal.Curse)); discardCards.AddRange(cards.Where(c => c.Category.HasFlag(Categories.Ruins))); discardCards.AddRange(cards.Where(c => c is Cards.DarkAges.Hovel)); discardCards.AddRange(cards.Where(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate)); discardCards.AddRange(cards.Where(c => c is Cards.DarkAges.Rats || c is Cards.DarkAges2ndEdition.Rats)); discardCards.AddRange(cards.Where(c => (c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && c.Type != Cards.Universal.TypeClass.Estate && c.Type != Cards.Universal.TypeClass.Province))); if (cards.Any(c => c is Cards.Universal.Estate)) discardCards.AddRange(cards.Where(c => c is Cards.Universal.Estate).Take(cards.Count(c => c is Cards.Universal.Estate) - cards.Count(c => c is Cards.Intrigue.Baron))); if (cards.Any(c => c is Cards.Universal.Province)) discardCards.AddRange(cards.Where(c => c is Cards.Universal.Province).Take(cards.Count(c => c is Cards.Universal.Province) - cards.Count(c => c is Cards.Cornucopia.Tournament || c is Cards.Cornucopia2ndEdition.Tournament))); if (GameProgressLeft < 0.63) discardCards.AddRange(cards.Where(c => c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece)); discardCards = discardCards.Distinct().ToList(); switch (choice.ChoiceOutcome) { case ChoiceOutcome.Select: if (discardCards.Count >= 3) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceOutcome.Discard: return new ChoiceResult(new CardCollection(discardCards.Take(3))); } return base.DecideTheSkysGift(choice); } protected override ChoiceResult DecideTheSunsGift(Choice choice) { if (choice.ChoiceOutcome == ChoiceOutcome.Discard) { var averageDrawPileValue = RealThis.DrawPile.Count == 0 ? 0 : RealThis.DrawPile.LookThrough(c => true).Sum(c => ComputeValueInDeck(c)) / RealThis.DrawPile.Count; var toDiscard = FindBestCardsToDiscard(choice.Cards, choice.Maximum) .TakeWhile( c => c.Category.HasFlag(Categories.Victory) && !c.Category.HasFlag(Categories.Action) && !c.Category.HasFlag(Categories.Treasure) && ComputeValueInDeck(c) > 0.85 * averageDrawPileValue ).Take(2); return new ChoiceResult(new CardCollection(toDiscard)); } var sentryCards = new CardCollection(choice.Cards); // Order them in roughly random order sentryCards.Shuffle(); return new ChoiceResult(sentryCards); } protected override ChoiceResult DecideTheWindsGift(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } protected override ChoiceResult DecideThief(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.ChoiceOutcome == ChoiceOutcome.Trash) { return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); } // Always gain all Treasure cards else if (choice.ChoiceOutcome == ChoiceOutcome.Gain) { // Except Copper, Masterpiece var ccThief = new CardCollection(choice.Cards.Where(c => !(c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece))); var coppers = RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper || c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece); var allTreasures = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure)); var 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 is Cards.Prosperity.Loan); return new ChoiceResult(ccThief); } return base.DecideThief(choice); } protected override ChoiceResult DecideThroneRoom(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cardsToPlay = new CardCollection(); var bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierExclusionPredicate())); // OK, nothing good found. Now let's allow not-so-useful cards to be played if (bestCard == null) bestCard = FindBestCardToPlay(choice.Cards.Where(MultiplierDestructiveExclusionPredicate())); if (bestCard != null) cardsToPlay.Add(bestCard); else if (choice.Minimum > 0) cardsToPlay.Add(choice.Cards.ElementAt(_Game.RNG.Next(choice.Cards.Count()))); return new ChoiceResult(cardsToPlay); } protected override ChoiceResult DecideTournament(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // Always reveal a Province if I can return new ChoiceResult(new List { choice.Options[0].Text }); 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 (GameProgressLeft < 0.4 && ((ISupply)RealThis._Game.Table[Cards.Universal.TypeClass.Duchy]).Any()) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Duchy); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Cornucopia.TrustySteed || c is Cards.Cornucopia2ndEdition.TrustySteed); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Cornucopia.Followers || c is Cards.Cornucopia2ndEdition.Followers); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Cornucopia.Princess); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Cornucopia.BagOfGold || c is Cards.Cornucopia2ndEdition.BagOfGold); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Cornucopia.Diadem); if (bestCard == null) bestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Duchy); if (bestCard != null) return new ChoiceResult(new CardCollection { bestCard }); return new ChoiceResult(new CardCollection()); default: return base.DecideTournament(choice); } } protected override ChoiceResult DecideToil(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cards = new CardCollection(); var cardToPlay = FindBestCardToPlay(choice.Cards); if (cardToPlay != null) cards.Add(cardToPlay); return new ChoiceResult(cards); } protected override ChoiceResult DecideTorturer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: var torturerCrapCards = RealThis.Hand.Count(card => card is Cards.Universal.Copper || card.Category.HasFlag(Categories.Victory) && !card.Category.HasFlag(Categories.Action) && !card.Category.HasFlag(Categories.Treasure) || card.Category.HasFlag(Categories.Curse) || card.Category.HasFlag(Categories.Ruins) || card is Cards.Guilds.Masterpiece || card is Cards.Guilds2ndEdition.Masterpiece ); // Choose to take a Curse if there aren't any left // or if we have a Watchtower or Trader in hand if (((ISupply)RealThis._Game.Table.TableEntities[Cards.Universal.TypeClass.Curse]).Count == 0 || RealThis.Hand[c => c is Cards.Prosperity.Watchtower || c is Cards.Prosperity2ndEdition.Watchtower].Any() || RealThis.Hand[c => c is Cards.Hinterlands.Trader || c is Cards.Hinterlands2ndEdition.Trader].Any()) return new ChoiceResult(new List { choice.Options[1].Text }); // 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 if (torturerCrapCards >= 2 || RealThis.Hand.Count == torturerCrapCards) return new ChoiceResult(new List { choice.Options[0].Text }); // Choose to take on a Curse return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); default: return base.DecideTorturer(choice); } } protected override ChoiceResult DecideTrade(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var tradeToTrash = new CardCollection(); // Always choose to trash all Curses tradeToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Curse).Take(2)); // Always choose to trash all Ruins if (tradeToTrash.Count < 2) tradeToTrash.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Ruins)).Take(2 - tradeToTrash.Count)); // Always choose to trash all Hovels if (tradeToTrash.Count < 2) tradeToTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.Hovel).Take(2 - tradeToTrash.Count)); // Let's trash Rocks and Masterpiece as well if (tradeToTrash.Count < 2) tradeToTrash.AddRange(choice.Cards.Where(c => c is Cards.Empires.Rocks).Take(2 - tradeToTrash.Count)); if (tradeToTrash.Count < 2) tradeToTrash.AddRange(choice.Cards.Where(c => c is Cards.Guilds.Masterpiece || c is Cards.Guilds2ndEdition.Masterpiece).Take(2 - tradeToTrash.Count)); if (tradeToTrash.Count < 2) tradeToTrash.AddRange(choice.Cards.Where(c => c is Cards.Universal.Copper).Take(2 - tradeToTrash.Count)); if (tradeToTrash.Count < 2) tradeToTrash.AddRange(choice.Cards.Where(c => c is Cards.DarkAges.OvergrownEstate || c is Cards.DarkAges2ndEdition.OvergrownEstate).Take(2 - tradeToTrash.Count)); return new ChoiceResult(tradeToTrash); } protected override ChoiceResult DecideTrader(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always trash Curses if we can var traderBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Curse)); // Always trash Ruins if we can if (traderBestCard == null) traderBestCard = choice.Cards.FirstOrDefault(c => c.Category.HasFlag(Categories.Ruins)); // Trash Copper later in the game -- they just suck if (traderBestCard == null && GameProgressLeft < 0.75) traderBestCard = choice.Cards.FirstOrDefault(c => c is Cards.Universal.Copper); if (traderBestCard == null) traderBestCard = FindBestCardsToTrash(choice.Cards.Where(c => !c.Category.HasFlag(Categories.Victory)), 1).FirstOrDefault(); if (traderBestCard == null) traderBestCard = FindBestCardsToTrash(choice.Cards, 1).ElementAt(0); if (traderBestCard != null) return new ChoiceResult(new CardCollection { traderBestCard }); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideTradeRoute(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideTradingPost(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 2))); } protected override ChoiceResult DecideTragicHero(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideTraining(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var maxCards = 0; var maxCardsSupplies = new List(); var supplies = choice.Supplies.Values.OfType(); foreach (var supply in supplies) { var count = RealThis.CountAll(RealThis, c => supply.Types.Contains(c.Type)); if (count > maxCards) maxCards = count; if (count == maxCards) maxCardsSupplies.Add(supply); } return new ChoiceResult(FindBestCardForCost(maxCardsSupplies.Any() ? maxCardsSupplies : supplies, null, false)); } protected override ChoiceResult DecideTransport(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // Nothing we want to topdeck, so exile from the Supply if (!RealThis.PlayerMats.ContainsKey(Cards.Menagerie.TypeClass.Exile) || !RealThis.PlayerMats[Cards.Menagerie.TypeClass.Exile].Any(c => c.Category.HasFlag(Categories.Action) && ShouldPlay(c)) ) return new ChoiceResult(ResourcesHelper.Get("ExileActionSupply")); break; case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCards(choice.Cards, 1))); case ChoiceType.Supplies: // Very simple and can be improved quite a bit return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } return base.DecideTransport(choice); } protected override ChoiceResult DecideTransmogrify(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Text == Resource.ChooseACardToTrash) return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideTransmute(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); } protected override ChoiceResult DecideTrashingToken(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var toTrash = new CardCollection(); // Always choose to trash a Curse if possible if (choice.Cards.Any(c => c is Cards.Universal.Curse)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Curse) }); // Always choose to trash a Ruins if possible if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c.Category.HasFlag(Categories.Ruins)) }); // Always choose to trash Hovel if possible if (choice.Cards.Any(c => c is Cards.DarkAges.Hovel)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.DarkAges.Hovel) }); // If there are no Curses left, choose to trash Sea Hag or Familiar if possible if (_Game.Table.Curse.Count == 0 && choice.Cards.Any(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar) }); // Always choose to trash Copper if we have at least 2 Golds or at least 5 Silvers if ((RealThis.CountAll(RealThis, c => c is Cards.Universal.Gold) >= 2 || RealThis.CountAll(RealThis, c => c is Cards.Universal.Silver) >= 5) && choice.Cards.Any(c => c is Cards.Universal.Copper)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Universal.Copper) }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideTreasurer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // #1 Priority is Platinum or Fortune in the trash if (_Game.Table.Trash.Any(c => c is Cards.Prosperity.Platinum || c is Cards.Empires.Fortune)) return new ChoiceResult(new List { choice.Options[1].Text }); // #2 Priority is if someone else has the Key -- take it from them var possessor = FindPossessorOf(Cards.Renaissance.TypeClass.Key); if (possessor != null && possessor != RealThis) return new ChoiceResult(new List { choice.Options[2].Text }); // #3 Priority is Gold, Bank, Harem, or Hoard in the trash if (_Game.Table.Trash.Any(c => c is Cards.Universal.Gold || c is Cards.Intrigue.Harem || c is Cards.Prosperity.Bank || c is Cards.Prosperity.Hoard)) return new ChoiceResult(new List { choice.Options[1].Text }); // #4 Priority is if we don't have the Key if (possessor != null && possessor != RealThis) return new ChoiceResult(new List { choice.Options[2].Text }); // #5 Priority is Philosopher's Stone, Spoils, Diadem, Royal Seal, Venture, Cache, Counterfeit, Ill-Gotten Gains, Relic, // Treasure Trove, Plunder, Capital, Charm, Crown, Idol, Scepter, Spices, or Stash in the trash if (_Game.Table.Trash.Any(c => (c.BaseCost == new Cost(5) || c.BaseCost.Potion > 0 || c is Cards.DarkAges.Spoils || c is Cards.Cornucopia.Diadem) && !(c is Cards.Prosperity.Contraband))) return new ChoiceResult(new List { choice.Options[1].Text }); // #6 Priority is cards we'd like to trash var trashableTreasures = FindBestCardsToTrash(RealThis.Hand[Categories.Treasure], 1, true); if (trashableTreasures.Any()) return new ChoiceResult(new List { choice.Options[0].Text }); // #7 Priority is not-terrible Treasures if (_Game.Table.Trash.Any(c => c is Cards.Universal.Silver || c is Cards.Empires.Rocks || c is Cards.Nocturne.LuckyCoin)) return new ChoiceResult(new List { choice.Options[1].Text }); // We don't want to gain anything from the trash or trash a Treasure from our hand, so choose to take the Key return new ChoiceResult(new List { choice.Options[2].Text }); case ChoiceType.Cards: switch (choice.ChoiceOutcome) { case ChoiceOutcome.Trash: return new ChoiceResult(new CardCollection(FindBestCardsToTrash(choice.Cards, 1))); case ChoiceOutcome.Gain: return new ChoiceResult(new ItemCollection { FindBestCards(choice.Cards, 1).First() }); } break; } return base.DecideTreasurer(choice); } protected override ChoiceResult DecideTrustySteed(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // 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. var trustySteedChoices = new List { choice.Options[0].Text }; if (RealThis.Hand[Categories.Action].Count >= 2) trustySteedChoices.Add(choice.Options[1].Text); else { var actionCardsAvailable = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action), onlyCurrentlyDrawable: true); var totalCards = RealThis.DrawPile.Count + RealThis.DiscardPile.Count; double chanceOfGettingActions; if (RealThis.Hand[Categories.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].Text); else trustySteedChoices.Add(choice.Options[2].Text); } return new ChoiceResult(trustySteedChoices); } protected override ChoiceResult DecideUniversity(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideUpgrade(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); 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.OfType(), null, false)); default: return base.DecideUpgrade(choice); } } protected override ChoiceResult DecideUrchin(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // Choose to trash Urchin roughly 1/3 the time if (_Game.RNG.Next(3) == 0) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceType.Cards: return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, choice.Cards.Count() - 4))); default: return base.DecideUrchin(choice); } } protected override ChoiceResult DecideVampire(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideVassal(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Only play it if we should ever play that card var trigger = choice.Triggers.First(); if (trigger is Card && ShouldPlay((Card)trigger)) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideVault(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); switch (choice.ChoiceType) { case ChoiceType.Options: // If there are at least 2 non-Action & non-Treasure cards, discard 2 of them IEnumerable vaultDiscardableCards = RealThis.Hand[c => !c.Category.HasFlag(Categories.Treasure) && !c.Category.HasFlag(Categories.Action)]; if (vaultDiscardableCards.Count() >= 2) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); case ChoiceType.Cards: if (choice.Text.StartsWith("Discard any number of cards", StringComparison.InvariantCulture)) { // 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.HasFlag(Categories.Treasure)))); } // "Choose 2 cards to discard" var vDiscards = new CardCollection(); vDiscards.AddRange(choice.Cards.Where(c => c.Category.HasFlag(Categories.Curse))); if (vDiscards.Count < 2) vDiscards.AddRange(choice.Cards.Where(card => card.Category.HasFlag(Categories.Ruins))); if (vDiscards.Count < 2) vDiscards.AddRange(choice.Cards.Where(card => card.Category.HasFlag(Categories.Victory) && !card.Category.HasFlag(Categories.Action) && !card.Category.HasFlag(Categories.Treasure))); if (vDiscards.Count > 2) vDiscards.RemoveRange(2, vDiscards.Count - 2); else if (vDiscards.Count < 2) vDiscards.AddRange(FindBestCardsToDiscard(choice.Cards.Where(c => !vDiscards.Contains(c)), 2 - vDiscards.Count)); return new ChoiceResult(vDiscards); default: return base.DecideVault(choice); } } protected override ChoiceResult DecideVillageGreen(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // If we have more Actions we want to play than Actions available, take the Village effect now if (ActionsAvailable() - RealThis.Hand[c => ShouldPlay(c)].Count < 0) return new ChoiceResult(new List { choice.Options[0].Text }); // Now return new ChoiceResult(new List { choice.Options[1].Text }); // Next turn } protected override ChoiceResult DecideVillain(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 1))); } protected override ChoiceResult DecideWanderingMinstrel(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Just sort everything best-to-worst var cards = new CardCollection(choice.Cards); cards.Sort(new Cards.Sorting.ByCost(Cards.Sorting.SortDirection.Descending)); return new ChoiceResult(cards); } protected override ChoiceResult DecideWarehouse(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 3))); } protected override ChoiceResult DecideWatchtower(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // Always trash Curse & Copper cards from a Watchtower if (choice.Triggers[0].Type == Cards.Universal.TypeClass.Curse || choice.Triggers[0].Type == Cards.Universal.TypeClass.Copper) return new ChoiceResult(new List { choice.Options[0].Text }); // Almost always trash Ruins (only if we have Death Cart do we want to keep them) if (choice.Triggers[0].Category.HasFlag(Categories.Ruins) && RealThis.CountAll(RealThis, c => c is Cards.DarkAges.DeathCart || c is Cards.DarkAges2019Errata.DeathCart) == 0) return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideWayfarer(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var coinValue = ComputeAverageCoinValueInDeck(); if (ComputeAverageCoinValueInDeck() < 1.5) // If Silver increases our deck's potency return new ChoiceResult(new List { choice.Options[0].Text }); return new ChoiceResult(new List { choice.Options[1].Text }); } protected override ChoiceResult DecideWildHunt(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); // If there are no Estates, always draw cards if (_Game.Table.Estate.Count == 0) return new ChoiceResult(new List { choice.Options[0].Text }); // +3 Cards // The real question comes down to what's the cutoff for drawing cards or gaining points // As a rough guess, I think that number is somewhere around 4 VPs if (_Game.Table.TableEntities[Cards.Empires.TypeClass.WildHunt].Tokens.Count(t => t is Cards.Prosperity.VictoryToken) >= _Game.RNG.Next(3, 5)) return new ChoiceResult(new List { choice.Options[1].Text }); // +3 Cards return new ChoiceResult(new List { choice.Options[0].Text }); } protected override ChoiceResult DecideWish(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideWishingWell(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); var cardsCount = new Dictionary(); foreach (var type in CardsGained) cardsCount[type] = (int)Math.Pow(RealThis.CountAll(RealThis, c => c.Type == type, false, true), 2); // Choose one at random, with a probability based on the cards left to be able to draw var indexChosen = _Game.RNG.Next(cardsCount.Sum(kvp => kvp.Value)); Card wishingWellCard = null; foreach (var type in cardsCount.Keys) { if (cardsCount[type] == 0) continue; if (indexChosen < cardsCount[type]) { var wishingWellSupply = choice.Supplies.Select(kvp => kvp.Value).OfType().FirstOrDefault(s => s.Type == type); if (wishingWellSupply != null) return new ChoiceResult(wishingWellSupply); wishingWellCard = choice.Cards.FirstOrDefault(c => c.Type == type); break; } indexChosen -= cardsCount[type]; } if (wishingWellCard != null) return new ChoiceResult(new CardCollection { wishingWellCard }); return base.DecideWishingWell(choice); } protected override ChoiceResult DecideWorkshop(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideYoungWitch(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(new CardCollection(FindBestCardsToDiscard(choice.Cards, 2))); } protected override ChoiceResult DecideZombieApprentice(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (choice.Cards.Any(c => c.Category.HasFlag(Categories.Ruins))) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c.Category.HasFlag(Categories.Ruins)) }); if (_Game.Table.Curse.Count == 0 && choice.Cards.Any(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.Seaside.SeaHag || c is Cards.Seaside2ndEdition.SeaHag || c is Cards.Alchemy.Familiar) }); if (choice.Cards.Any(c => c is Cards.DarkAges.Necropolis)) return new ChoiceResult(new CardCollection { choice.Cards.First(c => c is Cards.DarkAges.Necropolis) }); return new ChoiceResult(new CardCollection()); } protected override ChoiceResult DecideZombieMason(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); return new ChoiceResult(FindBestCardForCost(choice.Supplies.Values.OfType(), null, false)); } protected override ChoiceResult DecideZombieSpy(Choice choice) { Contract.Requires(choice != null, "choice cannot be null"); if (IsCardOkForMeToDiscard(choice.Triggers.OfType().First())) return new ChoiceResult(new List { choice.Options[0].Text }); // Discard return new ChoiceResult(new List { choice.Options[1].Text }); // Put back } } }