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 AIRix : Standard { public new static string AIName => "Action Hero"; public new static string AIDescription => "Similar to Big Money, but opens up buying to Action cards."; public AIRix(Game game, string name) : base(game, name) { } public AIRix(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; // Never buy Potions (or *now* really never buy more than 1) if ((buyable.TopCard.Type == Cards.Alchemy.TypeClass.Potion) && (RealThis.CountAll(RealThis, c => c is Cards.Alchemy.Potion) > 0)) 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); } protected override IBuyable FindBestCardToBuy(List buyables) { Contract.Requires(buyables != null, "buyables cannot be null"); //this.Currency var allCards = RealThis.CountAll(); var fGameProgress = GameProgressLeft; var allFloat = (float)RealThis.CountAll(); var fActionCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Action)) / allFloat; var fCurseCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Curse)) / allFloat; var fTreasureCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Treasure)) / allFloat; var fVictoryCards = RealThis.CountAll(RealThis, c => c.Category.HasFlag(Categories.Victory)) / allFloat; var fBenifitCardCards = RealThis.CountAll(RealThis, c => c.Benefit.Cards > 0) / allFloat; var fBenifitActionCards = RealThis.CountAll(RealThis, c => c.Benefit.Actions > 0) / allFloat; 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 + 0.75f * buyable.TopCard.BaseCost.Debt.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 (fGameProgress < 0.2 && !buyable.Category.HasFlag(Categories.Victory)) score *= 0.4d; // combo : buy +actions if we have lots of +cards. var supply = buyable as ISupply; if (supply != null && (supply.Benefit.Actions >= 2) && (fGameProgress > 0.5) && (fBenifitCardCards > 2 * fBenifitActionCards + 0.05d)) { score *= 2d; } // buy attack cards early in the game. if (buyable.Category.HasFlag(Categories.Attack) && (fGameProgress > 0.8) && (fBenifitCardCards > 3 * fBenifitActionCards)) { score *= 2d; } if (buyable.Category.HasFlag(Categories.Victory) && !buyable.Category.HasFlag(Categories.Action) && !buyable.Category.HasFlag(Categories.Treasure)) { // Never buy non-Province/Colony/Victory-only cards early if (fGameProgress > 0.81 && 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; // mid-game scale back medium victory cards if ((fGameProgress > 0.30 && 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.RealThis.CurrentTurn.CardsBought.Count(c => c is Cards.Universal.Copper) > 1) // score = -1d; } // 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 || 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.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.45d; // 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 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.1d) 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(); if (bestScore >= 0d) return scores[bestScore][_Game.RNG.Next(scores[bestScore].Count)]; return null; } } }