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 RixAttack : Standard { public new static string AIName => "Interaction"; public new static string AIDescription => "Likes to buy Attack cards early and middle game."; public RixAttack(Game game, string name) : base(game, name) { } public RixAttack(Game game, string name, Player realThis) : base(game, name, realThis) { } public override float GameProgressLeft => GameProgressNew; protected override bool ShouldBuy(IBuyable buyable) { Contract.Requires(buyable != null, "buyable cannot be null"); if (!buyable.CanBuy(RealThis)) return false; var fGameProgress = GameProgressLeft; if (buyable.TopCard == null) return false; // Never buy Potions if (buyable.TopCard.Type == Cards.Alchemy.TypeClass.Potion) return false; // Special Action cards without +Cards that are allowed if (buyable.TopCard.Type == Cards.Base.TypeClass.Adventurer || buyable.TopCard.Type == Cards.Intrigue.TypeClass.Nobles || buyable.TopCard.Type == Cards.Intrigue.TypeClass.SecretChamber || buyable.TopCard.Type == Cards.Prosperity.TypeClass.CountingHouse || buyable.TopCard.Type == Cards.Prosperity2ndEdition.TypeClass.CountingHouse || buyable.TopCard.Type == Cards.Nocturne.TypeClass.NightWatchman || buyable.TopCard.Type == Cards.Nocturne.TypeClass.Crypt || buyable.TopCard.Type == Cards.Nocturne.TypeClass.DenOfSin || buyable.TopCard.Type == Cards.Nocturne.TypeClass.Raider ) return true; // Allow all Treasure & Victory cards if (buyable.TopCard.Category.HasFlag(Categories.Treasure) || buyable.TopCard.Category.HasFlag(Categories.Victory)) return true; // Also allow cards that provide +Cards if (buyable.TopCard is Card card && (card.Benefit.Cards > 0 || card.DurationBenefit.Cards > 0)) return true; return base.ShouldBuy(buyable); } //internal int CountAll(Player fromPlayer, Predicate predicate, Boolean onlyObtainable, Boolean onlyCurrentlyDrawable) private float GetDeckTreasureBuySum() { bool predicate(IPoints c) => c.Category.HasFlag(Categories.Treasure); int selector(IPoints c) => (c as Card)?.Benefit.Currency.Coin.Value ?? 0; return RealThis.SumAll(RealThis, predicate, selector); } protected override IBuyable FindBestCardToBuy(List buyables) { Contract.Requires(buyables != null, "buyables cannot be null"); var allCards = RealThis.CountAll(); var fGameProgress = GameProgressLeft; var fAll = (float)RealThis.CountAll(); var fActionCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action)) / fAll; var fCurseCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Curse)) / fAll; var fTreasureCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure)) / fAll; var fVictoryCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Victory)) / fAll; var fBenifitCardCards = RealThis.CountAll(RealThis, c => c.Benefit.Cards > 0) / fAll; var fBenifitActionCards = RealThis.CountAll(RealThis, c => c.Benefit.Actions > 0) / fAll; var fTreasureIndex = GetDeckTreasureBuySum() / fAll; //(float)this(this, c => c.Category.HasFlag(Category.Treasure)) / fAll; var fStealTreasureCards = RealThis.CountAll(RealThis, c => (c is Cards.Base.Thief) || c is Cards.Seaside.PirateShip || c is Cards.Seaside2ndEdition.PirateShip || c is Cards.Hinterlands.NobleBrigand || c is Cards.Hinterlands2ndEdition.NobleBrigand) / fAll; var bMoatInPlay = RealThis._Game.Table.TableEntities.Any(kvp => kvp.Key == Cards.Base.TypeClass.Moat || kvp.Key == Cards.Base2ndEdition.TypeClass.Moat || kvp.Key == Cards.Seaside.TypeClass.Lighthouse || kvp.Key == Cards.Seaside2ndEdition.TypeClass.Lighthouse); // System.Diagnostics.Debug.WriteLine("Moat:"+bMoatInPlay.ToString()); System.Diagnostics.Debug.WriteLine("all:{0}, Game:{6:0.00} A{1:0.00}, T{2:0.00}:{7:0.00}, V{3:0.00}, BC{4:0.00}, BA{5:0.00}", fAll, fActionCards, fTreasureCards, fVictoryCards, fBenifitCardCards, fBenifitActionCards, fGameProgress, fTreasureIndex); var scores = new Dictionary>(); foreach (var buyable in buyables) { // We need to compute score based on the original/base cost of the card double fBaseCost = buyable.BaseCost.Coin.Value + 2.5f * buyable.BaseCost.Potion.Value + 0.75f * buyable.TopCard.BaseCost.Debt.Value; var score = fBaseCost; // Scale based on the original -vs- current cost -- cheaper cards should be more valuable to us! // score += Math.Log((score + 1) / (supply.CurrentCost.Coin.Value + 2.5f * supply.CurrentCost.Potion.Value + 1)); if (!ShouldBuy(buyable)) score = -1d; // buy attack cards early in the game. if (((buyable.Category.HasFlag(Categories.Attack)) || (buyable.Type == Cards.Seaside.TypeClass.Embargo || buyable.Type == Cards.Seaside2ndEdition.TypeClass.Embargo)) && (fGameProgress > 0.2)) //if (((buyable.Category.HasFlag(Categories.Attack)) || (buyable.Type == Cards.Seaside.TypeClass.Embargo || buyable.Type == Cards.Seaside2ndEdition.TypeClass.Embargo || buyable.Type == Cards.Seaside2019Errata.TypeClass.Embargo)) && (fGameProgress > 0.2)) { if ( // don;t want list. (buyable.Type == Cards.Seaside.TypeClass.Ambassador) || // only good with curses (buyable.Type == Cards.Seaside2ndEdition.TypeClass.Ambassador) || // only good with curses (buyable.Type == Cards.Cornucopia.TypeClass.Jester) || // nice card, but difficult to play. (buyable.Type == Cards.Cornucopia2ndEdition.TypeClass.Jester) || // nice card, but difficult to play. (buyable.Type == Cards.Prosperity.TypeClass.Mountebank) || (buyable.Type == Cards.Prosperity2ndEdition.TypeClass.Mountebank) || (buyable.Type == Cards.Seaside.TypeClass.Cutpurse) || (buyable.Type == Cards.Seaside2ndEdition.TypeClass.Cutpurse) || (buyable.Type == Cards.Cornucopia.TypeClass.FortuneTeller) || (buyable.Type == Cards.Cornucopia2ndEdition.TypeClass.FortuneTeller) ) score = -1d; if (fActionCards < 0.3) // 3 in 10 action cards is enough. { if ( //3 ((buyable.Type == Cards.Base.TypeClass.Bureaucrat) && (fGameProgress > 0.8)) || // 1 or 2 of these early game is enough. ((buyable.Type == Cards.Base2ndEdition.TypeClass.Bureaucrat) && (fGameProgress > 0.8)) || // 1 or 2 of these early game is enough. //3P (buyable.Type == Cards.Alchemy.TypeClass.Familiar) || //2 not an attack card but nice all the same. (buyable.Type == Cards.Seaside.TypeClass.Embargo) || (buyable.Type == Cards.Seaside2ndEdition.TypeClass.Embargo) || //(buyable.Type == Cards.Seaside2019Errata.TypeClass.Embargo) || //5 (buyable.Type == Cards.Seaside.TypeClass.GhostShip) || (buyable.Type == Cards.Seaside2ndEdition.TypeClass.GhostShip) || //6 (buyable.Type == Cards.Prosperity.TypeClass.Goons) || (buyable.Type == Cards.Prosperity2ndEdition.TypeClass.Goons) || //5 (buyable.Type == Cards.Hinterlands.TypeClass.Margrave) || //4 (buyable.Type == Cards.Base.TypeClass.Militia) || (buyable.Type == Cards.Base2ndEdition.TypeClass.Militia) || //5 (buyable.Type == Cards.Intrigue.TypeClass.Minion) || // can the choice be done? (buyable.Type == Cards.Intrigue2ndEdition.TypeClass.Minion) || // can the choice be done? //3 (buyable.Type == Cards.Hinterlands.TypeClass.Oracle) || // can the choice be done? (buyable.Type == Cards.Hinterlands2ndEdition.TypeClass.Oracle) || // can the choice be done? //4 (buyable.Type == Cards.Seaside.TypeClass.PirateShip) || // can the choice be done? (buyable.Type == Cards.Seaside2ndEdition.TypeClass.PirateShip) || // can the choice be done? //5 (buyable.Type == Cards.Prosperity.TypeClass.Rabble) || (buyable.Type == Cards.Prosperity2ndEdition.TypeClass.Rabble) || //5 (buyable.Type == Cards.Intrigue.TypeClass.Saboteur) || //2P (buyable.Type == Cards.Alchemy.TypeClass.ScryingPool) || // 1 of these is great. (buyable.Type == Cards.Alchemy2ndEdition.TypeClass.ScryingPool) || // 1 of these is great. //4 (buyable.Type == Cards.Seaside.TypeClass.SeaHag) || (buyable.Type == Cards.Seaside2ndEdition.TypeClass.SeaHag) || //5 (buyable.Type == Cards.Intrigue.TypeClass.Torturer) || (buyable.Type == Cards.Intrigue2ndEdition.TypeClass.Torturer) || //5 (buyable.Type == Cards.Base.TypeClass.Witch) || (buyable.Type == Cards.Base2ndEdition.TypeClass.Witch) || //4 (buyable.Type == Cards.Cornucopia.TypeClass.YoungWitch) || (buyable.Type == Cards.Cornucopia2ndEdition.TypeClass.YoungWitch) ) { var fActionMult = (10.0f * fBaseCost * fGameProgress * fGameProgress) / (fActionCards + 0.5f); //global::System.Windows.Forms.MessageBox.Show(fActionMult.ToString()); score *= fActionMult; if (score < 0) // there maybe some unrecommened cards from ShouldBuy() score = -score; } // better in mid-game. if ( //4 (buyable.Type == Cards.Hinterlands.TypeClass.NobleBrigand) || // can the choice be done? (buyable.Type == Cards.Hinterlands2ndEdition.TypeClass.NobleBrigand) || // can the choice be done? //4 (buyable.Type == Cards.Base.TypeClass.Thief) ) { var fActionMult = (8.0f * fBaseCost * fGameProgress) / (fActionCards + 0.5f); //global::System.Windows.Forms.MessageBox.Show(fActionMult.ToString()); score *= fActionMult; if (score < 0) // there maybe some unrecommened cards from ShouldBuy() score = -score; } // less attractive attack cards. //4 - spy already gets a bonus from +cards, it overwhelms other 4 priced attacks. //3 - swindler not too many as low cost if (buyable.Type == Cards.Base.TypeClass.Spy || buyable.Type == Cards.Intrigue.TypeClass.Swindler || buyable.Type == Cards.Intrigue2ndEdition.TypeClass.Swindler) { var fActionMult = (2.0f * fBaseCost * fGameProgress * fGameProgress) / (fActionCards + 0.5f); //global::System.Windows.Forms.MessageBox.Show(fActionMult.ToString()); score *= fActionMult; if (score < 0) // there maybe some unrecommened cards from ShouldBuy() score = -score; } } } // Scale back the score accordingly if it's near the end of the game and the card is not a Victory card if (fGameProgress < 0.25 && !buyable.Category.HasFlag(Categories.Victory)) score *= 0.1d; // Never buy non-Province/Colony/Victory-only cards early if (fGameProgress > 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 ((fGameProgress > 0.15 && buyable.Type == Cards.Universal.TypeClass.Estate) || (fGameProgress > 0.30 && buyable.Type == Cards.Alchemy.TypeClass.Vineyard) || (fGameProgress > 0.30 && buyable.Type == Cards.Alchemy2ndEdition.TypeClass.Vineyard) || (fGameProgress > 0.40 && buyable.Type == Cards.Base.TypeClass.Gardens) || (fGameProgress > 0.40 && buyable.Type == Cards.Base2ndEdition.TypeClass.Gardens) || (fGameProgress > 0.40 && buyable.Type == Cards.Hinterlands.TypeClass.SilkRoad) || (fGameProgress > 0.40 && buyable.Type == Cards.Hinterlands2ndEdition.TypeClass.SilkRoad) || (fGameProgress > 0.45 && buyable.Type == Cards.Universal.TypeClass.Duchy) || (fGameProgress > 0.45 && buyable.Type == Cards.Intrigue.TypeClass.Duke) || (fGameProgress > 0.45 && buyable.Type == Cards.Cornucopia.TypeClass.Fairgrounds) || (fGameProgress > 0.45 && buyable.Type == Cards.Cornucopia2ndEdition.TypeClass.Fairgrounds) ) score *= 0.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; } 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.CurrentTurn.CardsBought.Count(c => c is Cards.Universal.Copper) > 1) // score = -1d; } //can get the money using steal cards. if ((fStealTreasureCards > 0.05) && (!bMoatInPlay)) { if ((buyable.Type == Cards.Universal.TypeClass.Silver) || (buyable.Type == Cards.Universal.TypeClass.Gold)) { System.Diagnostics.Debug.WriteLine($"Lowering Gold & Silver due to steal, fStealTreasureCards:{fStealTreasureCards:0.00} : {(fTreasureIndex + 1.0f):0.00}"); score /= (fTreasureIndex + 1.0f); } } // 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); } // 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) { score *= 0.4f; } // 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.Empires.TypeClass.Triumph) { if (!_Game.Table.Estate.CanGain()) score = -1.0; } // 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); 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, supplyCard.Benefit.Cards - 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, supplyCard.Benefit.Cards - 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) { // Royal Blacksmith is bad early or if we've got a lot of Coppers 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 //fBenifitActionCards, fActionCards score *= Math.Pow(1.075 + fActionCards - fBenifitActionCards, supplyCard.Benefit.Cards); // change slightly to make more actions when we need them. //score *= Math.Pow(1.075, supplyCard.Benefit.Cards); } if (supplyCard.Benefit.Buys > 0) score *= 1.025; score *= 0.995; var allActionCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action)); if ((double)allActionCards / allCards > 0.3d) score *= 0.4; // Final adjustment for Attack cards if (buyable.Category.HasFlag(Categories.Attack)) score *= 1.12d; } 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(); var OrderedScores = scores.Keys.OrderByDescending(k => k); foreach (var s in OrderedScores) { for (var i = 0; i < scores[s].Count; i++) System.Diagnostics.Debug.WriteLine("score:{1:0.00}, card:{0}", scores[s][i].Type.Name, s); } if (bestScore >= 0d) return scores[bestScore][_Game.RNG.Next(scores[bestScore].Count)]; return null; } } }