using DominionBase.Enums; using DominionBase.Properties; using DominionBase.Utilities; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.Contracts; using System.Linq; using System.Runtime; using System.Xml.Serialization; namespace DominionBase.Cards { [Serializable] public enum ConstraintType { [Description("Select constraint")] [ToolTip("Blank constraint that doesn't do anything")] Unknown, [Description("Must use Card")] [ToolTip("The card listed must be used")] CardMustUse, [Description("Cannot use Card")] [ToolTip("The card listed cannot be used")] CardDontUse, [Description("Card is in Set")] [ToolTip("The card was released in the Set listed")] SetIs, [Description("Card Type has")] [ToolTip("The card's Type has listed Type in its Types")] CategoryContains, [Description("Card costs")] [ToolTip("The card costs exactly the listed amount")] CardCosts, [Description("Card cost contains Potion")] [ToolTip("The card cost has Potion in it")] CardCostContainsPotion, [Description("Card cost contains Debt")] [ToolTip("The card cost has Debt in it")] CardCostContainsDebt, [Description("Card has Trait")] [ToolTip("The card has the Trait listed")] HasTrait, [Description("Sets to use cards from")] [ToolTip("Use cards from the number of sets listed")] NumberOfSets, //[Description("Cards per set")] //[ToolTip("Use this many cards per set")] //CardsPerSet, } public class ToolTipAttribute : Attribute { private string _Text; /// Summary: /// Specifies the default value for the DominionBase.ToolTip, /// which is an empty string (""). This static field is read-only. public static readonly ToolTipAttribute Default; /// Summary: /// Initializes a new instance of the DominionBase.ToolTip /// class with no parameters. public ToolTipAttribute() : this(string.Empty) { } /// Summary: /// Initializes a new instance of the DominionBase.ToolTip /// class with a tooltip. /// /// Parameters: /// tooltip: /// The tooltip text. [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public ToolTipAttribute(string text) { _Text = text; } /// Summary: /// Gets the tooltip stored in this attribute. /// /// Returns: /// The tooltip stored in this attribute. public virtual string Text => _Text; /// Summary: /// Gets or sets the string stored as the tooltip. /// /// Returns: /// The string stored as the tooltip. The default value is an empty string /// (""). protected string TextValue { get { return _Text; } set { _Text = value; } } /// Summary: /// Returns whether the value of the given object is equal to the current DominionBase.ToolTip. /// /// Parameters: /// obj: /// The object to test the value equality of. /// /// Returns: /// true if the value of the given object is equal to that of the current; otherwise, /// false. public override bool Equals(object obj) { if (obj == this) return true; return obj is ToolTipAttribute toolTip && toolTip.Text == Text; } public override int GetHashCode() { return Text.GetHashCode(); } /// Summary: /// Returns a value indicating whether this is the default DominionBase.ToolTip /// instance. /// /// Returns: /// true, if this is the default DominionBase.ToolTip instance; /// otherwise, false. public override bool IsDefaultAttribute() { return false; } } public class ConstraintException : Exception { public ConstraintException() { } public ConstraintException(string message) : base(message) { } public ConstraintException(string message, Exception innerException) : base(message, innerException) { } internal ConstraintException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } } [Serializable] public class Constraint { private ConstraintType _ConstraintType = ConstraintType.Unknown; private Object _ConstraintValue; private int _AbsoluteRangeMin; private int _AbsoluteRangeMax = 10; private int _RangeMin; private int _RangeMax = 10; private int _Minimum; private int _Maximum = 10; private static List ccAllCards; public int Minimum { get { return _Minimum; } set { _Minimum = value; if (_Minimum < RangeMin) _Minimum = RangeMin; if (Maximum < _Minimum) Maximum = _Minimum; } } public int Maximum { get { return _Maximum; } set { _Maximum = value; if (_Maximum > RangeMax) _Maximum = RangeMax; if (Minimum > _Maximum) Minimum = _Maximum; } } public ConstraintType ConstraintType { get { return _ConstraintType; } set { if (_ConstraintType == value) return; var resetValue = (_ConstraintType == ConstraintType.CardMustUse && value != ConstraintType.CardDontUse) || (_ConstraintType == ConstraintType.CardDontUse && value != ConstraintType.CardMustUse); _ConstraintType = value; if (resetValue) ConstraintValue = null; RangeMin = AbsoluteRangeMin; RangeMax = AbsoluteRangeMax; switch (value) { case ConstraintType.CardDontUse: Minimum = Maximum = 0; break; case ConstraintType.CardMustUse: Minimum = Maximum = 1; break; case ConstraintType.NumberOfSets: if (ccAllCards == null) ccAllCards = new List(CardCollection.GetAllCards(c => c.Location == Location.Kingdom || c.Location == Location.LandscapeCard)); RangeMin = 1; RangeMax = Math.Min(RangeMax, ccAllCards.GroupBy(c => c.Source).Count()); break; } } } public object ConstraintValue { get { return _ConstraintValue; } set { switch (ConstraintType) { case ConstraintType.CategoryContains: if (value is KeyValuePair kvpCategories) _ConstraintValue = kvpCategories.Key; else if (value != null) _ConstraintValue = (Categories)value; else _ConstraintValue = Categories.Action; break; case ConstraintType.SetIs: if (value is KeyValuePair kvpSource) _ConstraintValue = kvpSource.Key; else if (value != null) _ConstraintValue = (Source)value; else _ConstraintValue = Source.Base; break; case ConstraintType.CardCosts: if (value is KeyValuePair kvpCost) _ConstraintValue = kvpCost.Key; else _ConstraintValue = value; break; case ConstraintType.HasTrait: if (value is KeyValuePair kvpTraits) _ConstraintValue = kvpTraits.Key; else _ConstraintValue = value; break; default: _ConstraintValue = value; break; } } } [XmlIgnore] public int RangeMin { get { return _RangeMin; } set { _RangeMin = value; if (_RangeMin > _Minimum) Minimum = _RangeMin; } } [XmlIgnore] public int RangeMax { get { return _RangeMax; } set { _RangeMax = value; if (_RangeMax < _Maximum) Maximum = _RangeMax; } } [XmlIgnore] public int AbsoluteRangeMin { get { return _AbsoluteRangeMin; } set { _AbsoluteRangeMin = value; RangeMin = value; } } [XmlIgnore] public int AbsoluteRangeMax { get { return _AbsoluteRangeMax; } set { _AbsoluteRangeMax = value; RangeMax = value; } } public Constraint() : this(ConstraintType.Unknown, null, 0, 10) { } public Constraint(ConstraintType constraintType, string cardName) { ConstraintType = constraintType; switch (constraintType) { case ConstraintType.CardDontUse: ConstraintValue = cardName; break; case ConstraintType.CardMustUse: ConstraintValue = cardName; break; default: throw new ArgumentException(Resource.ConstraintTypeNotAllowed); } } public Constraint(ConstraintType constraintType, int minimum, int maximum) { switch (constraintType) { case ConstraintType.NumberOfSets: break; default: throw new ArgumentException(Resource.ConstraintTypeNotAllowed); } ConstraintType = constraintType; Minimum = 1; if (ccAllCards == null) ccAllCards = new List(CardCollection.GetAllCards(c => c.Location == Location.Kingdom)); Maximum = ccAllCards.GroupBy(c => c.Source).Count(); } public Constraint(ConstraintType constraintType, object constraintValue, int minimum, int maximum) { ConstraintType = constraintType; ConstraintValue = constraintValue; Minimum = minimum; Maximum = maximum; } internal bool Matches(IRandomizable card) { return PredicateFunction(card); } internal bool MinimumMet(IEnumerable chosenCards) { switch (ConstraintType) { case ConstraintType.NumberOfSets: return chosenCards.GroupBy(GroupingFunction).Count() >= Minimum; //case Cards.ConstraintType.CardsPerSet: // return chosenCards.GroupBy(this.GroupingFunction).All(g => g.Count() >= this.Minimum); default: return chosenCards.Count(PredicateFunction) >= Minimum; } } internal bool IsChoosable(IEnumerable chosenCards, IRandomizable card) { switch (ConstraintType) { case ConstraintType.NumberOfSets: var groupsNOS = chosenCards.GroupBy(GroupingFunction); return (groupsNOS.Count() < Maximum || (groupsNOS.Count() == Maximum && groupsNOS.Count(g => g.Key == card.Source.ToString()) == 1)); //case Cards.ConstraintType.CardsPerSet: // IEnumerable> groupsCPS = chosenCards.GroupBy(this.GroupingFunction); // return !groupsCPS.Any(g => g.Key == card.Source.ToString()) || groupsCPS.FirstOrDefault(g => g.Key == card.Source.ToString()).Count() < this.Maximum; default: return (chosenCards.Count(PredicateFunction) < Maximum); } } public IEnumerable GetMatchingCards(IEnumerable cardsAvailable) { return cardsAvailable.Where(PredicateFunction); } public IEnumerable GetCardsToDiscard(IEnumerable selectedCards, IEnumerable cardsAvailable) { switch (ConstraintType) { case ConstraintType.NumberOfSets: return cardsAvailable.Where(card => selectedCards.Any(c => card.Source == c.Source)); //case Cards.ConstraintType.CardsPerSet: // return _CardsAvailable.Where(card => selectedCards.Count(c => card.Source == c.Source) >= this.Maximum); default: return selectedCards; } } private Func PredicateFunction { get { switch (ConstraintType) { case ConstraintType.CardMustUse: case ConstraintType.CardDontUse: if (ConstraintValue is IRandomizable cvRand) return card => cvRand.Type == card.Type; if (ConstraintValue is string cvString) return card => cvString == card.Name; return card => true; case ConstraintType.SetIs: return card => card.Source == (Source)ConstraintValue; case ConstraintType.CategoryContains: return card => card.Category.HasFlag((Categories)ConstraintValue); case ConstraintType.CardCosts: return card => card.BaseCost == (Cost)ConstraintValue; case ConstraintType.CardCostContainsPotion: return card => card.BaseCost.Potion > 0; case ConstraintType.CardCostContainsDebt: return card => card.BaseCost.Debt > 0; case ConstraintType.HasTrait: return card => card.Traits.HasFlag((Traits)ConstraintValue); default: return card => true; } } } private Func GroupingFunction { get { switch (ConstraintType) { case ConstraintType.NumberOfSets: //case Cards.ConstraintType.CardsPerSet: return card => card.Source.ToString(); default: return card => card.Name; } } } } [Serializable] public class ConstraintCollection : List, INotifyCollectionChanged { [field: NonSerialized] public event NotifyCollectionChangedEventHandler CollectionChanged; private int _MaxCount = 10; public int MaxCount { get { return _MaxCount; } set { _MaxCount = value; foreach (var constraint in this) constraint.RangeMax = value; } } public ConstraintCollection() { } public ConstraintCollection(int maxCount) { MaxCount = maxCount; } public ConstraintCollection(IEnumerable collection) : base(collection) { } public bool IsChoosable(IEnumerable cardCollection, IRandomizable card) { return this.All(constraint => !constraint.Matches(card) || constraint.IsChoosable(cardCollection, card)); } public new void Add(Constraint item) { Contract.Requires(item != null, "item cannot be null"); item.AbsoluteRangeMax = MaxCount; base.Add(item); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); } public new void AddRange(IEnumerable collection) { Contract.Requires(collection != null, "collection cannot be null"); foreach (var item in collection) item.AbsoluteRangeMax = MaxCount; base.AddRange(collection); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection)); } public new void Insert(int index, Constraint item) { Contract.Requires(item != null, "item cannot be null"); item.AbsoluteRangeMax = MaxCount; base.Insert(index, item); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); } public new void Remove(Constraint item) { base.Remove(item); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item)); } public new void Sort() { Sort((c1, c2) => -c1.Minimum.CompareTo(c2.Minimum)); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } internal bool MinimumMet(List cardCollection) { return this.All(constraint => constraint.MinimumMet(cardCollection)); } protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke(this, e); } public IList SelectCards(IList availableCards, int numberCardsToSelect) { var cardsChosen = new List(); // Remove all "CardDontUse" constraint cards first IList usableCards = new List(availableCards); foreach (var constraint in this.Where(c => c.ConstraintType == ConstraintType.CardDontUse)) foreach (var card in constraint.GetMatchingCards(availableCards)) usableCards.Remove(card); var usableConstraints = this.Where(c => c.ConstraintType != ConstraintType.CardDontUse); var constraintCards = new Dictionary>(); foreach (var constraint in usableConstraints) constraintCards[constraint] = new List(constraint.GetMatchingCards(availableCards)); var attempts = 0; // Satisfy Minimum constraints first do { attempts++; cardsChosen.Clear(); // Add in required cards first foreach (var constraint in usableConstraints.Where(c => c.ConstraintType == ConstraintType.CardMustUse)) cardsChosen.AddRange(constraintCards[constraint]); if (cardsChosen.Count > numberCardsToSelect) throw new ConstraintException( $"Too many required cards specified in constraints! Please double-check your setup and loosen the requirements. {numberCardsToSelect} needed & found {cardsChosen.Count} required constraints."); foreach (var constraint in usableConstraints.OrderByDescending(c => c.Minimum)) { if (constraint.MinimumMet(cardsChosen)) continue; var discardCards = new List(); constraintCards[constraint].Shuffle(); foreach (var card in constraintCards[constraint]) { if (discardCards.Contains(card)) continue; if (IsChoosable(cardsChosen, card)) { cardsChosen.Add(card); if (constraint.MinimumMet(cardsChosen)) break; // Certain constraints (like NumberOfSets) immediately disallow other cards when a card is chosen. // We need to get the list of disallowed cards after each card is added to the list. // This is only needed to ensure the Minimum constraint without taking 10k iterations discardCards.AddRange(constraint.GetCardsToDiscard(cardsChosen, constraintCards[constraint])); } } } // Give it 50 attempts at trying to satisfy the Minimum requirements if (attempts > 50) throw new ConstraintException(Resource.CannotSatisfyConstraints); } while (!MinimumMet(cardsChosen) || cardsChosen.Count > numberCardsToSelect); // After satisfying the Minimums, Maximums should be pretty easy to handle var cardsChosenNeeded = new List(cardsChosen); attempts = 0; while (cardsChosen.Count < numberCardsToSelect) { attempts++; // Give it 50 attempts at trying to satisfy the Minimum requirements if (attempts > 50) throw new ConstraintException(Resource.CannotSatisfyConstraints); cardsChosen.Clear(); cardsChosen.AddRange(cardsChosenNeeded); usableCards.Shuffle(); foreach (var chosenCard in usableCards) { if (cardsChosen.Contains(chosenCard)) continue; if (IsChoosable(cardsChosen, chosenCard)) cardsChosen.Add(chosenCard); if (cardsChosen.Count == numberCardsToSelect) break; } } cardsChosen.Shuffle(); return cardsChosen; } } }