using DominionBase.Cards; using DominionBase.Enums; using System; using System.Diagnostics.Contracts; using System.Linq; using System.Xml; namespace DominionBase.Players.AI.Bots { public enum BotRuleConditionOperandType { Unknown, Constant, CountAllCardsInDeck, CountCardsInDeck, CountCardsInSupply, CountCardsInHand, CountCardsInPlay, CountCardsLeftInDrawDeck, CountEmptyPiles, CountCardsLeftInSmallestPile, CountOnTavernMat, CountCardTypeInDeck, CountAvailableCoins, CountBuysLeft, CountTurns, CountTotalCoins, CountTotalCoinsExcludingNativeVillage, CountVP, CountMaxOpponentVP, IsActionPhase, IsPlusOneActionTokenSet, IsPlusOneCardTokenSet, IsPlusOneCoinTokenSet, IsPlusOneBuyTokenSet, IsMinusTwoCoinTokenSet, IsTrashingTokenSet, IsEstateTokenSet, IsTravellingFairActive, IsOpponentSwampHagInPlay, TokensOnDefiledShrine, GainsNeededToEndGame } public enum BotRuleConditionExtraOperandType { Unknown, Add, Subtract, Multiply, Divide } public class BotRuleConditionOperand { public BotRuleConditionOperandType OperandType { get; } public string RawValue { get; } public decimal Value { get; private set; } public BotRuleConditionExtraOperandType ExtraOperandType { get; } = BotRuleConditionExtraOperandType.Unknown; public string ExtraRawValue { get; } public BotRuleConditionOperand(XmlNode node, XmlNode extraNode = null) { if (node?.Attributes == null) return; RawValue = Bot.MapCardName(node.Attributes["attribute"]?.Value); OperandType = GetBotRuleConditionOperandType(node.Attributes["type"]?.Value); if (extraNode?.Attributes != null) { ExtraRawValue = Bot.MapCardName(extraNode.Attributes["attribute"]?.Value); ExtraOperandType = GetBotRuleConditionExtraOperandType(extraNode.Attributes["type"]?.Value); } } private BotRuleConditionOperandType GetBotRuleConditionOperandType(string value) { if (Enum.TryParse(value, true, out BotRuleConditionOperandType _out)) return _out; switch (value.ToUpperInvariant()) { case "ISTRASHINGTOKENPLACED": return BotRuleConditionOperandType.IsTrashingTokenSet; case "ISESTATETOKENPLACED": return BotRuleConditionOperandType.IsEstateTokenSet; case "COUNTAVAILABLEMONEY": return BotRuleConditionOperandType.CountAvailableCoins; case "ISMINUS$2TOKENSET": return BotRuleConditionOperandType.IsMinusTwoCoinTokenSet; case "ISPLUS$1TOKENSET": return BotRuleConditionOperandType.IsPlusOneCoinTokenSet; case "GETTOTALMONEY": return BotRuleConditionOperandType.CountTotalCoins; case "GETTOTALMONEYEXCLUDINGNATIVEVILLAGE": return BotRuleConditionOperandType.CountTotalCoinsExcludingNativeVillage; default: throw new Exception(); } } private BotRuleConditionExtraOperandType GetBotRuleConditionExtraOperandType(string value) { if (Enum.TryParse(value, true, out BotRuleConditionExtraOperandType _out)) return _out; switch (value.ToUpperInvariant()) { case "PLUS": return BotRuleConditionExtraOperandType.Add; case "MINUS": return BotRuleConditionExtraOperandType.Subtract; case "MULTIPLYWITH": return BotRuleConditionExtraOperandType.Multiply; case "DIVIDEBY": return BotRuleConditionExtraOperandType.Divide; default: throw new Exception(); } } public object Evaluate(IPlayer player) { Contract.Requires(player != null, "player cannot be null"); decimal decOut; switch (OperandType) { case BotRuleConditionOperandType.Unknown: Value = 0; break; case BotRuleConditionOperandType.Constant: if (decimal.TryParse(RawValue, out decOut)) Value = decOut; break; case BotRuleConditionOperandType.CountAllCardsInDeck: Value = player.CountAll(); break; case BotRuleConditionOperandType.CountCardsInDeck: Value = player.CountAll(predicate: c => c.Name == RawValue); break; case BotRuleConditionOperandType.CountCardsInSupply: Value = player._Game.Table.TableEntities.FirstOrDefault(te => te.Value.Name == RawValue).Value.Count; break; case BotRuleConditionOperandType.CountCardsInHand: Value = player.Hand[RawValue].Count; break; case BotRuleConditionOperandType.CountCardsInPlay: Value = player.InPlayAndSetAside[RawValue].Count; break; case BotRuleConditionOperandType.CountCardsLeftInDrawDeck: Value = player.DrawPile.Count; break; case BotRuleConditionOperandType.CountEmptyPiles: Value = player._Game.Table.EmptySupplyPiles; break; case BotRuleConditionOperandType.CountCardsLeftInSmallestPile: Value = player._Game.Table.TableEntities.Where(te => te.Value.Count > 0).Min(te => te.Value.Count); break; case BotRuleConditionOperandType.CountOnTavernMat: Value = player.PlayerMats.ContainsKey(Cards.Adventures.TypeClass.TavernMat) ? player.PlayerMats[Cards.Adventures.TypeClass.TavernMat][RawValue].Count : 0; break; case BotRuleConditionOperandType.CountCardTypeInDeck: Categories cat; Value = Enum.TryParse(RawValue, out cat) ? player.CountAll(predicate: c => c.Category.HasFlag(cat)) : 0; break; case BotRuleConditionOperandType.CountAvailableCoins: Value = player.Currency.Coin.Value + player.TokenPiles[Cards.Guilds.TypeClass.Coffer].Count - player.Currency.Debt.Value; break; case BotRuleConditionOperandType.CountBuysLeft: Value = player.Buys; break; case BotRuleConditionOperandType.CountTurns: Value = player._Game.UnmodifiedTurnsTaken.Count(t => t.Player == player); break; case BotRuleConditionOperandType.CountTotalCoins: Value = Convert.ToDecimal(SumCosts(player)); break; case BotRuleConditionOperandType.CountTotalCoinsExcludingNativeVillage: Value = Convert.ToDecimal(SumCosts(player, true)); break; case BotRuleConditionOperandType.CountVP: Value = 0; // TODO -- logic to count VPs break; case BotRuleConditionOperandType.CountMaxOpponentVP: Value = 0; // TODO -- logic to count VPs break; case BotRuleConditionOperandType.IsActionPhase: Value = player.Phase == PhaseEnum.Action ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.IsPlusOneActionTokenSet: Value = player._Game.Table.TableEntities.Any( te => te.Value.Tokens.Any( t => t is Cards.Adventures.PlusOneActionToken poaToken && poaToken.Owner == player)) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.IsPlusOneCardTokenSet: Value = player._Game.Table.TableEntities.Any( te => te.Value.Tokens.Any( t => t is Cards.Adventures.PlusOneCardToken pocToken && pocToken.Owner == player)) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.IsPlusOneCoinTokenSet: Value = player._Game.Table.TableEntities.Any( te => te.Value.Tokens.Any( t => t is Cards.Adventures.PlusOneCoinToken pocToken && pocToken.Owner == player)) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.IsPlusOneBuyTokenSet: Value = player._Game.Table.TableEntities.Any( te => te.Value.Tokens.Any( t => t is Cards.Adventures.PlusOneBuyToken pobToken && pobToken.Owner == player)) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.IsMinusTwoCoinTokenSet: Value = player._Game.Table.TableEntities.Any( te => te.Value.Tokens.Any( t => t is Cards.Adventures.MinusTwoCostToken mtcToken && mtcToken.Owner == player)) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.IsTrashingTokenSet: Value = player._Game.Table.TableEntities.Any( te => te.Value.Tokens.Any( t => t is Cards.Adventures.TrashingToken tToken && tToken.Owner == player)) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.IsEstateTokenSet: Value = 0.0m; // TODO -- Inheritance isn't implemented, so this will have to come later break; case BotRuleConditionOperandType.IsTravellingFairActive: Value = player.CurrentTurn.CardsBought.Any(c => c is Cards.Adventures.TravellingFair || c is Cards.Adventures2ndEdition.TravellingFair) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.IsOpponentSwampHagInPlay: Value = player._Game.Players.Where(p => p != player) .Any(p => p.InPlayAndSetAside[c => c is Cards.Adventures.SwampHag || c is Cards.Adventures2ndEdition.SwampHag].Any()) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.TokensOnDefiledShrine: Value = player._Game.Table.TableEntities.ContainsKey(Cards.Empires.TypeClass.DefiledShrine) && player._Game.Table[Cards.Empires.TypeClass.DefiledShrine].Tokens.Any( t => t.Type == Cards.Prosperity.TypeClass.VictoryToken) ? 1.0m : 0.0m; break; case BotRuleConditionOperandType.GainsNeededToEndGame: var pcCount = Math.Min(player._Game.Table.Province.Count, player._Game.Table.TableEntities.ContainsKey(Cards.Prosperity.TypeClass.Colony) ? player._Game.Table[Cards.Prosperity.TypeClass.Colony].Count : player._Game.Table.Province.Count); var smCount = player._Game.Table.TableEntities.Values.Select(t => t.Count).OrderBy(v => v).Take(player._Game.Players.Count <= 4 ? 3 : 4).Sum(); Value = Math.Min(pcCount, smCount); break; } switch (ExtraOperandType) { case BotRuleConditionExtraOperandType.Unknown: break; case BotRuleConditionExtraOperandType.Add: if (decimal.TryParse(ExtraRawValue, out decOut)) Value += decOut; break; case BotRuleConditionExtraOperandType.Subtract: if (decimal.TryParse(ExtraRawValue, out decOut)) Value -= decOut; break; case BotRuleConditionExtraOperandType.Multiply: if (decimal.TryParse(ExtraRawValue, out decOut)) Value *= decOut; break; case BotRuleConditionExtraOperandType.Divide: if (decimal.TryParse(ExtraRawValue, out decOut)) Value /= decOut; break; } return Value; } private static double SumCosts(IPlayer player, bool onlyCurrentlyDrawable = false) { return player.SumAll(player, card => true, card => { if (!(card is Card cardC)) return 0d; var val = cardC.Benefit.Currency.Coin.Value + 0.5 * cardC.DurationBenefit.Currency.Coin.Value; if (card is Cards.Base.Adventurer) val = 2; if (card is Cards.Base.Moneylender) val = 1.5; if (card is Cards.Base.Mine) val = 1; if (card is Cards.Intrigue.Baron) val = 2; if (card is Cards.Intrigue.Bridge) val = 2; if (card is Cards.Intrigue.Coppersmith) val = 2; if (card is Cards.Intrigue.MiningVillage) val = 1; if (card is Cards.Intrigue.Minion) val = 1.875; if (card is Cards.Intrigue.Pawn) val = 1; if (card is Cards.Intrigue.SecretChamber) val = 2; if (card is Cards.Intrigue.Steward) val = 1.75; if (card is Cards.Intrigue2ndEdition.Courtier) val = 2.5; if (card is Cards.Seaside.PirateShip || card is Cards.Seaside2ndEdition.PirateShip) val = player.TokenPiles[Cards.Seaside.TypeClass.PirateShipToken].Count; if (card is Cards.Alchemy.PhilosophersStone || card is Cards.Alchemy2ndEdition.PhilosophersStone) val = player.CountAll(onlyCurrentlyDrawable: true) / 5; if (card is Cards.Prosperity.Bank) val = 3; if (card is Cards.Prosperity.TradeRoute || card is Cards.Prosperity2ndEdition.TradeRoute) val = player._Game.Table.TokenPiles[Cards.Prosperity.TypeClass.TradeRouteToken].Count - 1; if (card is Cards.Prosperity.Vault || card is Cards.Prosperity2ndEdition.Vault) val = 3; if (card is Cards.Prosperity.Venture) val = 2; if (card is Cards.Cornucopia.Harvest) val = 2.5; if (card is Cards.Cornucopia.Princess) val = 2; if (card is Cards.Cornucopia.TrustySteed || card is Cards.Cornucopia2ndEdition.TrustySteed) val = 2; if (card is Cards.Hinterlands.FoolsGold || card is Cards.Hinterlands2ndEdition.FoolsGold) val = 2; if (card is Cards.Hinterlands.IllGottenGains || card is Cards.Hinterlands2ndEdition.IllGottenGains) val = 1.5; if (card is Cards.Hinterlands.Highway) val = 1; if (card is Cards.Guilds.Baker || card is Cards.Guilds2ndEdition.Baker) val = 1; if (card is Cards.Guilds.Butcher || card is Cards.Guilds2ndEdition.Butcher) val = 1.5; if (card is Cards.Guilds.CandlestickMaker || card is Cards.Guilds2ndEdition.CandlestickMaker) val = 1; if (card is Cards.DarkAges.Count || card is Cards.DarkAges2ndEdition.Count) val = 2; if (card is Cards.DarkAges.Counterfeit) val = 2; if (card is Cards.DarkAges.DeathCart) val = 3.5; if (card is Cards.DarkAges.Forager || card is Cards.DarkAges2ndEdition.Forager) val = player._Game.Table.Trash.Where(c => c.Category.HasFlag(Categories.Treasure)) .DistinctBy(c => c.Name) .Count(); if (card is Cards.DarkAges.Mercenary || card is Cards.DarkAges2ndEdition.Mercenary) val = 1.5; if (card is Cards.DarkAges.PoorHouse || card is Cards.DarkAges2ndEdition.PoorHouse) val = 2; if (card is Cards.Adventures.Amulet) val = 0.75; if (card is Cards.Adventures.Giant || card is Cards.Adventures2ndEdition.Giant) val = 3; if (card is Cards.Adventures.Miser) val = player.PlayerMats[Cards.Adventures.TypeClass.TavernMat].Count(c => c is Cards.Universal.Copper); if (card is Cards.Empires.Capital) val = 2; if (card is Cards.Empires.Charm) val = 2; if (card is Cards.Empires.FarmersMarket) val = 2; if (card is Cards.Empires.Fortune) val = 3; if (player._Game.Table.TableEntities.ContainsCardType(card) && player._Game.Table[card].Tokens.Any( t => t is Cards.Adventures.PlusOneCoinToken pocToken && pocToken.Owner == player)) val += 1; return val; }, onlyCurrentlyDrawable: onlyCurrentlyDrawable); } public override string ToString() { string output; switch (OperandType) { case BotRuleConditionOperandType.Constant: output = $"{RawValue}"; break; default: output = $"{OperandType}({RawValue})"; break; } switch (ExtraOperandType) { case BotRuleConditionExtraOperandType.Add: output = $"({output} + {ExtraRawValue})"; break; case BotRuleConditionExtraOperandType.Subtract: output = $"({output} - {ExtraRawValue})"; break; case BotRuleConditionExtraOperandType.Multiply: output = $"({output} * {ExtraRawValue})"; break; case BotRuleConditionExtraOperandType.Divide: output = $"({output} / {ExtraRawValue})"; break; } return output; } } }