using DominionBase.Cards; using DominionBase.Enums; using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; namespace DominionBase.Players.AI { public class BigMoney : Standard { public new static string AIName => "Big Money"; public new static string AIDescription => "Buys only Treasure and Victory cards. Very focused."; public BigMoney(Game game, string name) : base(game, name) { } public BigMoney(Game game, string name, Player realThis) : base(game, name, realThis) { } protected override bool ShouldBuy(IBuyable buyable) { Contract.Requires(buyable != null, "buyable cannot be null"); if (!buyable.CanBuy(RealThis)) return false; // Never buy Potions if (buyable.TopCard.Type == Cards.Alchemy.TypeClass.Potion) return false; // Only buy Treasure & Victory cards! if (buyable.TopCard.Category.HasFlag(Categories.Treasure) || buyable.TopCard.Category.HasFlag(Categories.Victory)) return true; return false; } protected override IBuyable FindBestCardToBuy(List buyables) { Contract.Requires(buyables != null, "buyables cannot be null"); var scores = new Dictionary>(); foreach (var buyable in buyables) { // We need to compute score based on the original/base cost of the card double score = buyable.BaseCost.Coin.Value + 2.5f * buyable.BaseCost.Potion.Value; // 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 + 2.5f * buyable.CurrentCost.Potion.Value + 1)); if (!ShouldBuy(buyable)) score = -1d; // Scale back the score accordingly if it's near the end of the game and the card is not a Victory card if (GameProgressLeft < 0.25 && !buyable.Category.HasFlag(Categories.Victory)) score *= 0.15d; // Scale up Province & Colony scores in late game if (GameProgressLeft < 0.3 && (buyable.Type == Cards.Universal.TypeClass.Province || buyable.Type == Cards.Prosperity.TypeClass.Colony)) score *= 1.2d; // Never buy non-Province/Colony/Victory-only cards early if (GameProgressLeft > 0.81 && buyable.Category.HasFlag(Categories.Victory) && !buyable.Category.HasFlag(Categories.Action) && !buyable.Category.HasFlag(Categories.Treasure) && buyable.Type != Cards.Universal.TypeClass.Province && buyable.Type != Cards.Prosperity.TypeClass.Colony && buyable.Type != Cards.Hinterlands.TypeClass.Farmland && buyable.Type != Cards.Hinterlands.TypeClass.Tunnel && buyable.Type != Cards.Hinterlands2ndEdition.TypeClass.Farmland && buyable.Type != Cards.Hinterlands2ndEdition.TypeClass.Tunnel ) score = -1d; if ((GameProgressLeft > 0.25 && buyable.Type == Cards.Universal.TypeClass.Estate) || (GameProgressLeft > 0.25 && buyable.Type == Cards.Alchemy.TypeClass.Vineyard) || (GameProgressLeft > 0.25 && buyable.Type == Cards.Alchemy2ndEdition.TypeClass.Vineyard) || (GameProgressLeft > 0.35 && buyable.Type == Cards.Base.TypeClass.Gardens) || (GameProgressLeft > 0.35 && buyable.Type == Cards.Base2ndEdition.TypeClass.Gardens) || (GameProgressLeft > 0.35 && buyable.Type == Cards.Hinterlands.TypeClass.SilkRoad) || (GameProgressLeft > 0.35 && buyable.Type == Cards.Hinterlands2ndEdition.TypeClass.SilkRoad) || (GameProgressLeft > 0.35 && buyable.Type == Cards.DarkAges.TypeClass.Feodum) || (GameProgressLeft > 0.35 && buyable.Type == Cards.DarkAges2ndEdition.TypeClass.Feodum) || (GameProgressLeft > 0.4 && buyable.Type == Cards.Universal.TypeClass.Duchy) || (GameProgressLeft > 0.4 && buyable.Type == Cards.Intrigue.TypeClass.Duke) || (GameProgressLeft > 0.4 && buyable.Type == Cards.Cornucopia.TypeClass.Fairgrounds) || (GameProgressLeft > 0.4 && buyable.Type == Cards.Cornucopia2ndEdition.TypeClass.Fairgrounds) ) score *= 0.15d; if ((GameProgressLeft < 0.15 && buyable.Type == Cards.Universal.TypeClass.Estate) || (GameProgressLeft < 0.15 && buyable.Type == Cards.Alchemy.TypeClass.Vineyard) || (GameProgressLeft < 0.15 && buyable.Type == Cards.Alchemy2ndEdition.TypeClass.Vineyard) || (GameProgressLeft < 0.20 && buyable.Type == Cards.Base.TypeClass.Gardens) || (GameProgressLeft < 0.20 && buyable.Type == Cards.Base2ndEdition.TypeClass.Gardens) || (GameProgressLeft < 0.20 && buyable.Type == Cards.Hinterlands.TypeClass.SilkRoad) || (GameProgressLeft < 0.20 && buyable.Type == Cards.Hinterlands2ndEdition.TypeClass.SilkRoad) || (GameProgressLeft < 0.20 && buyable.Type == Cards.DarkAges.TypeClass.Feodum) || (GameProgressLeft < 0.20 && buyable.Type == Cards.DarkAges2ndEdition.TypeClass.Feodum) || (GameProgressLeft < 0.25 && buyable.Type == Cards.Universal.TypeClass.Duchy) || (GameProgressLeft < 0.25 && buyable.Type == Cards.Intrigue.TypeClass.Duke) || (GameProgressLeft < 0.25 && buyable.Type == Cards.Cornucopia.TypeClass.Fairgrounds) || (GameProgressLeft < 0.25 && buyable.Type == Cards.Cornucopia2ndEdition.TypeClass.Fairgrounds) ) score *= 1.1d; // Duke/Duchy decision if (buyable.Type == Cards.Intrigue.TypeClass.Duke || buyable.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.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.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.Type == Cards.Hinterlands.TypeClass.SilkRoad || buyable.Type == Cards.Hinterlands2ndEdition.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.Type == Cards.DarkAges.TypeClass.Feodum || buyable.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.Type == Cards.Universal.TypeClass.Copper) { // Never buy Copper cards unless we have a Goons in play if (RealThis.InPlay[c => c is Cards.Prosperity.Goons || c is Cards.Prosperity2ndEdition.Goons].Count == 0) score = -1d; //else if (this.RealThis.CurrentTurn.CardsBought.Count(c => c is Cards.Universal.Copper) > 1) // score = -1d; } if (buyable.Type == Cards.Universal.TypeClass.Silver) { //int totalSilverCount = this.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); score *= Math.Pow(1.04, 2 * totalFeodumCount); if (totalFeodumCount > 0 && GameProgressLeft < 0.25) score /= Math.Max(GameProgressLeft, 0.08); } // 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 // Not really so much with Big Money -- should maybe not buy any of them... if (buyable.Type == Cards.Alchemy.TypeClass.Potion) { score *= Math.Pow(0.8, RealThis.CountAll(RealThis, c => c is Cards.Alchemy.Potion)); score *= 0.75 * Math.Pow(1.1, RealThis._Game.Table.TableEntities.Values.Count(s => s.BaseCost.Potion > 0)); } if (buyable.Type == Cards.Prosperity.TypeClass.Talisman || buyable.Type == Cards.Prosperity2ndEdition.TypeClass.Talisman) { score *= 0.29; } if (buyable.Type == Cards.Prosperity.TypeClass.Quarry) { score *= 0.29; } // 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.Type == Cards.Base.TypeClass.Witch || buyable.Type == Cards.Base2ndEdition.TypeClass.Witch) { score *= Math.Pow(0.8, (9.8d - (1d + Utilities.Gaussian.NextGaussian(_Game.RNG) / 12d) * Math.Sqrt(10) * Math.Sqrt(((double)RealThis._Game.Table.Curse.Count) / (RealThis._Game.Players.Count - 1))) * 4.3335 / 10); } // Peddler -- not really worth 8; scale it back if it's over 5 if (buyable.Type == Cards.Prosperity.TypeClass.Peddler) { if (buyable.CurrentCost.Coin > 5) score *= 0.6f; } // Horn of Plenty -- not very useful in most set-ups -- Worth is normally about 1/2 for this AI if (buyable.Type == Cards.Cornucopia.TypeClass.HornOfPlenty || buyable.Type == Cards.Cornucopia2ndEdition.TypeClass.HornOfPlenty) { score *= 0.6f; } // Silver -- this can sometimes flood a deck -- make sure we don't have too many if (buyable.Type == Cards.Universal.TypeClass.Silver) { var numSilvers = RealThis.CountAll(RealThis, c => c is Cards.Universal.Silver); var numBetterThanSilver = RealThis.CountAll(RealThis, c => c is Cards.Universal.Gold || c is Cards.Prosperity.Platinum || c is Cards.Prosperity.Venture || c is Cards.Prosperity.Hoard || c is Cards.Prosperity.Bank || c is Cards.Prosperity.RoyalSeal || c is Cards.Prosperity2ndEdition.RoyalSeal || c is Cards.Cornucopia.Diadem || c is Cards.Hinterlands.Cache || c is Cards.Hinterlands2ndEdition.Cache || c is Cards.Adventures.Relic || c is Cards.Adventures.TreasureTrove || c is Cards.Adventures2ndEdition.Relic || c is Cards.Empires.Capital || c is Cards.Empires.Charm || c is Cards.Empires.Crown || c is Cards.Empires.Fortune || c is Cards.Empires.Plunder || c is Cards.Nocturne.Idol || c is Cards.Promotional.Stash || c is Cards.Promotional2ndEdition.Stash ); var numCards = RealThis.CountAll(); if ((double)numBetterThanSilver / numCards > 0.05d && (double)numSilvers / numCards > 0.4d) score *= 0.1; } // Loan -- we don't want too many; at most, we should have 2 if (buyable.Type == Cards.Prosperity.TypeClass.Loan || buyable.Type == Cards.Prosperity2ndEdition.TypeClass.Loan) { var numLoans = RealThis.CountAll(RealThis, c => c is Cards.Prosperity.Loan || c is Cards.Prosperity2ndEdition.Loan); var numCountingHouses = RealThis.CountAll(RealThis, c => c is Cards.Prosperity.CountingHouse || c is Cards.Prosperity2ndEdition.CountingHouse); var numCoppers = RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper); if (numLoans >= 2 || numCountingHouses > 0) score *= 0.1; score *= Math.Pow(1.05, (numCoppers > 9 ? 9 : numCoppers) - 7); } // Limit the number of Contrabands we'll buy to a fairly small amount (1 per every 20 cards or so) if (buyable.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.5) score *= Math.Pow(0.2, Math.Pow(percentageOfContrabands, 2)); } // Need to be careful when buying this card, since it can muck up our deck (e.g. making us trash a Province or Colony) if (buyable.Type == Cards.Hinterlands.TypeClass.Farmland || buyable.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.Type == Cards.Guilds.TypeClass.Masterpiece || buyable.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 || c is Cards.Base2ndEdition.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); } Console.WriteLine("Masterpiece new score: {0}", score); } if (buyable.Type == Cards.Empires.TypeClass.Triumph) { var vpsGained = RealThis.CurrentTurn.CardsGained.Count + (_Game.Table.Estate.CanGain() ? 1 : 0); if (!_Game.Table.Estate.CanGain()) score = -1.0; if (vpsGained < 3) score *= 0.2; else score *= Math.Pow(1.1, vpsGained); } // The more +Cards an Action or Night card gives, the more worthwhile it is. // Almost all Action cards are worthless to this AI's strategy, so most things scale to near 0 // An absolute baseline is +2 Cards. Anything less than that isn't getting us anywhere, even if it has other bonuses if (buyable.Category.HasFlag(Categories.Action) || buyable.Category.HasFlag(Categories.Night)) { var supplyCard = Card.CreateInstance(buyable.Type); // A bird in the hand is worth 2 in the bush. Future +cards aren't quite a valuable as present +cards var scaledCards = supplyCard.Benefit.Cards + 0.8 * supplyCard.DurationBenefit.Cards; var allCards = RealThis.CountAll(); if (supplyCard.Benefit.Cards < 1) { // Special case for Adventurer -- it's not all that bad with Big Money if (buyable.Type == Cards.Base.TypeClass.Adventurer) score *= 0.8d; // Special case for Nobles -- it's got +3 Cards as an option else if (buyable.Type == Cards.Intrigue.TypeClass.Nobles) score *= Math.Pow(1.075, 3); // Special case for Secret Chamber -- it's good when there's a bunch of crap in your deck (Curse, Action, Victory cards) else if (buyable.Type == Cards.Intrigue.TypeClass.SecretChamber) { var allNonTreasureCards = RealThis.CountAll(RealThis, c => !c.Category.HasFlag(Categories.Treasure)); var allTreasureCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure)); if ((double)allNonTreasureCards / allTreasureCards > 0.5d) score *= 1.1d; } // We like Counting House with lots of Coppers! else if (buyable.Type == Cards.Prosperity.TypeClass.CountingHouse || buyable.Type == Cards.Prosperity2ndEdition.TypeClass.CountingHouse) score *= Math.Pow(1.05, 7 - RealThis.CountAll(RealThis, c => c is Cards.Universal.Copper)); // Special case for Watchtower -- "draw-to-6" is generally +2 cards, with the added benefit of being able to trash terrible cards instead of gaining them else if (buyable.Type == Cards.Prosperity.TypeClass.Watchtower || buyable.Type == Cards.Prosperity2ndEdition.TypeClass.Watchtower) score *= Math.Pow(1.075, 1.75); else if (buyable.Type == Cards.Adventures.TypeClass.Storyteller || buyable.Type == Cards.Adventures2ndEdition.TypeClass.Storyteller) score *= Math.Pow(1.075, 1.3); // Special case for Governor -- it's got +1(+3) Cards as an option else if (buyable.Type == Cards.Promotional.TypeClass.Governor || buyable.Type == Cards.Promotional2ndEdition.TypeClass.Governor) score *= Math.Pow(1.075, 2.75); // Special case for Wild Hunt -- it's got +3 Cards as an option else if (buyable.Type == Cards.Empires.TypeClass.WildHunt) score *= Math.Pow(1.075, 2.5); // Special case for Cursed Village -- "draw-to-6" is generally +2 cards & a Hex is a modest offset else if (buyable.Type == Cards.Nocturne.TypeClass.CursedVillage) score *= Math.Pow(1.075, 1.5); // If we have a decent percentage of Copper/trashable Treasures, Pooka is reliably +4 cards else 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 else 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; } // Special case for Werewolf -- it's got +3 Cards as an option (and can be played in Night if it's dead-drawn) else if (buyable.Type == Cards.Nocturne.TypeClass.Werewolf) score *= Math.Pow(1.075, 2.75); else score *= 0.1d; } else if (supplyCard.Benefit.Cards < 2) { if (supplyCard.DurationBenefit.Cards > 0) score *= 0.5d * Math.Pow(1.075, supplyCard.DurationBenefit.Cards); // Special case for Oasis -- It's not *ACTUALLY* +1 Card else if (buyable.Type == Cards.Hinterlands.TypeClass.Oasis) score *= 0.2d; else score *= 0.3d; } else { // Special case for Courtyard -- it's really only +2 Cards if (buyable.Type == Cards.Intrigue.TypeClass.Courtyard || buyable.Type == Cards.Intrigue2ndEdition.TypeClass.Courtyard) score *= Math.Pow(1.075, supplyCard.Benefit.Cards - 1); // Special case for Secret Passage -- It's only slightly better than a cantrip else if (buyable.Type == Cards.Intrigue2ndEdition.TypeClass.SecretPassage) score *= Math.Pow(1.075, supplyCard.Benefit.Cards - 1); // Special case for Warehouse -- It's not *ACTUALLY* +3 Cards else if (buyable.Type == Cards.Seaside.TypeClass.Warehouse) score *= 0.25d; // Special case for Envoy -- it's really only +3 Cards else if (buyable.Type == Cards.Promotional.TypeClass.Envoy || buyable.Type == Cards.Promotional2ndEdition.TypeClass.Envoy) score *= Math.Pow(1.075, supplyCard.Benefit.Cards - 1); // Special case for YoungWitch -- It's not *ACTUALLY* +2 Cards else if (buyable.Type == Cards.Cornucopia.TypeClass.YoungWitch || buyable.Type == Cards.Cornucopia2ndEdition.TypeClass.YoungWitch) score *= 0.25d; // Special case for Inn -- It's not *ACTUALLY* +2 Cards else if (buyable.Type == Cards.Hinterlands.TypeClass.Inn || buyable.Type == Cards.Hinterlands2ndEdition.TypeClass.Inn) score *= 0.25d; // Special case for Embassy -- it's really only +2 Cards else if (buyable.Type == Cards.Hinterlands.TypeClass.Embassy) score *= Math.Pow(1.075, supplyCard.Benefit.Cards - 2.5); // Special case for Fugutive -- it's only slightly better than +1 Card else if (buyable.Type == Cards.Adventures.TypeClass.Fugitive) score *= Math.Pow(1.075, scaledCards - 0.9); // Special case for Forum -- it's only slightly better than +1 Card else if (buyable.Type == Cards.Empires.TypeClass.Forum) score *= Math.Pow(1.075, scaledCards - 1.75); // Special case for City Quarter -- it's almost never worth it else if (buyable.Type == Cards.Empires.TypeClass.CityQuarter) score = 0.25d; // Special case for Royal Blacksmith -- it's worse based on the number of Coppers in our deck else if (buyable.Type == Cards.Empires.TypeClass.RoyalBlacksmith) { 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; } else score *= Math.Pow(1.075, supplyCard.Benefit.Cards); } if (supplyCard.Benefit.Buys > 0) score *= 1.05; var allActionCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action)); if ((double)allActionCards / allCards > 0.1d) score *= 0.4; // Final adjustment for Attack cards if (buyable.Category.HasFlag(Categories.Attack)) score *= 1.20d; } if (buyable.Category.HasFlag(Categories.Event)) score *= 0.5; 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 (!scores.ContainsKey(score)) scores[score] = new List(); scores[score].Add(buyable); } var bestScore = scores.Keys.OrderByDescending(k => k).First(); if (bestScore >= 0d) return scores[bestScore][_Game.RNG.Next(scores[bestScore].Count)]; return null; } } }