Skip to content

Commit

Permalink
Various solver optimizations, reduced runtime + memory usage
Browse files Browse the repository at this point in the history
  • Loading branch information
tylercamp committed May 6, 2024
1 parent 735ca3c commit dbe5800
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 39 deletions.
11 changes: 8 additions & 3 deletions PalCalc.Model/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,16 @@ public static V GetValueOrElse<K, V>(this IDictionary<K, V> dict, K key, V fallb

public static int SetHash<T>(this IEnumerable<T> elements)
{
var baseHash = elements.Count();
var baseHash = 0;
var total = 0;
foreach (var g in elements.GroupBy(e => e.GetHashCode()).OrderBy(g => g.Key))
baseHash = HashCode.Combine(baseHash, g.Key, g.Count());
{
var count = g.Count();
baseHash = HashCode.Combine(baseHash, g.Key, total);
total += count;
}

return baseHash;
return HashCode.Combine(baseHash, total);
}
}
}
48 changes: 36 additions & 12 deletions PalCalc.Solver/BreedingSolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,20 +102,46 @@ static List<PalInstance> RelevantInstancesForTraits(PalDB db, List<PalInstance>
// gender (if they're wild/bred pals) for the least overall effort (different pals have different gender probabilities)
(IPalReference, IPalReference) PreferredParentsGenders(IPalReference parent1, IPalReference parent2)
{
List<IPalReference> ParentOptions(IPalReference parent) => parent.Gender == PalGender.WILDCARD
? new List<IPalReference>() { parent.WithGuaranteedGender(db, PalGender.MALE), parent.WithGuaranteedGender(db, PalGender.FEMALE) }
: new List<IPalReference>() { parent };
IEnumerable<IPalReference> ParentOptions(IPalReference parent)
{
if (parent.Gender == PalGender.WILDCARD)
{
yield return parent.WithGuaranteedGender(db, PalGender.MALE);
yield return parent.WithGuaranteedGender(db, PalGender.FEMALE);
}
else
{
yield return parent;
}
}

var optionsParent1 = ParentOptions(parent1);
var optionsParent2 = ParentOptions(parent2);

var parentPairOptions = optionsParent1.SelectMany(p1v => optionsParent2.Where(p2v => p2v.IsCompatibleGender(p1v.Gender)).Select(p2v => (p1v, p2v))).ToList();
var parentPairOptions = optionsParent1
.SelectMany(p1v => optionsParent2.Where(p2v => p2v.IsCompatibleGender(p1v.Gender)).Select(p2v => (p1v, p2v)))
//.ToList()
;

Func<IPalReference, IPalReference, TimeSpan> CombinedEffortFunc = gameSettings.MultipleBreedingFarms
? ((a, b) => a.BreedingEffort > b.BreedingEffort ? a.BreedingEffort : b.BreedingEffort)
: ((a, b) => a.BreedingEffort + b.BreedingEffort);

if (parentPairOptions.Select(pair => CombinedEffortFunc(pair.p1v, pair.p2v)).Distinct().Count() == 1)
TimeSpan optimalTime = TimeSpan.Zero;
bool hasNoPreference = true;
foreach (var (p1, p2) in parentPairOptions)
{
var effort = CombinedEffortFunc(p1, p2);
if (optimalTime == TimeSpan.Zero) optimalTime = effort;
else if (optimalTime != effort)
{
hasNoPreference = false;

if (effort < optimalTime) optimalTime = effort;
}
}

if (hasNoPreference)
{
// either there is no preference or at least 1 parent already has a specific gender
var p1wildcard = parent1.Gender == PalGender.WILDCARD;
Expand Down Expand Up @@ -144,7 +170,7 @@ List<IPalReference> ParentOptions(IPalReference parent) => parent.Gender == PalG
}
else
{
return parentPairOptions.MinBy(p => CombinedEffortFunc(p.p1v, p.p2v));
return parentPairOptions.First(p => optimalTime == CombinedEffortFunc(p.p1v, p.p2v));
}
}

Expand Down Expand Up @@ -401,7 +427,7 @@ var traitGroup in palGroup

var combinedTraits = p.Item1.EffectiveTraits.Concat(p.Item2.EffectiveTraits);

var anyRelevantFromParents = spec.Traits.Intersect(combinedTraits).Any();
var anyRelevantFromParents = combinedTraits.Intersect(spec.Traits).Any();
var anyIrrelevantFromParents = combinedTraits.Except(spec.Traits).Any();

return anyRelevantFromParents || !anyIrrelevantFromParents;
Expand Down Expand Up @@ -440,19 +466,17 @@ var traitGroup in palGroup
// (not entirely correct, since some irrelevant traits may be specific and inherited by parents. if we know a child
// may have some specific trait, it may be efficient to breed that child with another parent which also has that
// irrelevant trait, which would increase the overall likelyhood of a desired trait being inherited)
var potentialIrrelevantTraits = Enumerable
.Range(0, Math.Max(0, numFinalTraits - desiredParentTraits.Count))
.Select(i => new RandomTrait());

var newTraits = new List<Trait>(numFinalTraits);
newTraits.AddRange(desiredParentTraits);
while (newTraits.Count < numFinalTraits)
newTraits.Add(new RandomTrait());

var res = new BredPalReference(
gameSettings,
db.BreedingByParent[parent1.Pal][parent2.Pal].Child,
preferredParent1,
preferredParent2,
desiredParentTraits.Concat(potentialIrrelevantTraits).ToList(),
newTraits,
probabilityForUpToNumTraits
);

Expand Down
79 changes: 56 additions & 23 deletions PalCalc.Solver/IPalReference.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using PalCalc.Model;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
Expand Down Expand Up @@ -131,14 +132,19 @@ public CompositeOwnedPalReference(OwnedPalReference male, OwnedPalReference fema

public TimeSpan SelfBreedingEffort { get; } = TimeSpan.Zero;

private CompositeOwnedPalReference oppositeWildcardReference;
public IPalReference WithGuaranteedGender(PalDB db, PalGender gender)
{
switch (gender)
{
case PalGender.MALE: return Male;
case PalGender.FEMALE: return Female;
case PalGender.OPPOSITE_WILDCARD: return new CompositeOwnedPalReference(Male, Female) { Gender = gender };
case PalGender.WILDCARD: return this;
case PalGender.OPPOSITE_WILDCARD:
if (oppositeWildcardReference == null)
oppositeWildcardReference = new CompositeOwnedPalReference(Male, Female) { Gender = gender };
return oppositeWildcardReference;

default: throw new NotImplementedException();
}
}
Expand Down Expand Up @@ -189,12 +195,8 @@ public int CapturesRequiredForGender

public int EffectiveTraitsHash { get; }

public IPalReference WithGuaranteedGender(PalDB db, PalGender gender)
private IPalReference WithGuaranteedGenderImpl(PalDB db, PalGender gender)
{
if (Gender != PalGender.WILDCARD) throw new Exception("Wild pal has already been given a guaranteed gender");

if (gender == PalGender.WILDCARD) return this;

return new WildPalReference(Pal)
{
SelfBreedingEffort = SelfBreedingEffort,
Expand All @@ -203,6 +205,26 @@ public IPalReference WithGuaranteedGender(PalDB db, PalGender gender)
};
}

private Dictionary<PalGender, IPalReference> cachedGuaranteedGenders = null;
public IPalReference WithGuaranteedGender(PalDB db, PalGender gender)
{
if (Gender != PalGender.WILDCARD) throw new Exception("Wild pal has already been given a guaranteed gender");

if (gender == PalGender.WILDCARD) return this;

if (cachedGuaranteedGenders == null)
{
cachedGuaranteedGenders = new List<PalGender>()
{
PalGender.MALE,
PalGender.FEMALE,
PalGender.OPPOSITE_WILDCARD
}.ToDictionary(g => g, g => WithGuaranteedGenderImpl(db, g));
}

return cachedGuaranteedGenders[gender];
}

public override string ToString() => $"Captured {Gender} {Pal} w/ up to {EffectiveTraits.Count} random traits";
}

Expand All @@ -227,6 +249,12 @@ private BredPalReference(GameSettings gameSettings, Pal pal, IPalReference paren
}
EffectiveTraits = traits;
EffectiveTraitsHash = traits.SetHash();

parentBreedingEffort = gameSettings.MultipleBreedingFarms && Parent1 is BredPalReference && Parent2 is BredPalReference
? Parent1.BreedingEffort > Parent2.BreedingEffort
? Parent1.BreedingEffort
: Parent2.BreedingEffort
: Parent1.BreedingEffort + Parent2.BreedingEffort;
}

public BredPalReference(GameSettings gameSettings, Pal pal, IPalReference parent1, IPalReference parent2, List<Trait> traits, float traitsProbability) : this(gameSettings, pal, parent1, parent2, traits)
Expand All @@ -250,14 +278,9 @@ public BredPalReference(GameSettings gameSettings, Pal pal, IPalReference parent

public int AvgRequiredBreedings { get; private set; }
public TimeSpan SelfBreedingEffort => AvgRequiredBreedings * gameSettings.AvgBreedingTime;
public TimeSpan BreedingEffort => SelfBreedingEffort + (
gameSettings.MultipleBreedingFarms && Parent1 is BredPalReference && Parent2 is BredPalReference
? Parent1.BreedingEffort > Parent2.BreedingEffort
? Parent1.BreedingEffort
: Parent2.BreedingEffort
: Parent1.BreedingEffort + Parent2.BreedingEffort

);
private TimeSpan parentBreedingEffort;
public TimeSpan BreedingEffort => SelfBreedingEffort + parentBreedingEffort;

private int numTotalBreedingSteps = -1;
public int NumTotalBreedingSteps
Expand All @@ -277,10 +300,8 @@ public int NumTotalBreedingSteps

public List<Trait> ActualTraits => EffectiveTraits;

public IPalReference WithGuaranteedGender(PalDB db, PalGender gender)
private IPalReference WithGuaranteedGenderImpl(PalDB db, PalGender gender)
{
if (this.Gender != PalGender.WILDCARD) throw new Exception("Cannot change gender of bred pal with an already-guaranteed gender");

if (gender == PalGender.WILDCARD)
{
return this;
Expand All @@ -298,14 +319,16 @@ public IPalReference WithGuaranteedGender(PalDB db, PalGender gender)
TraitsProbability = TraitsProbability,
};
}

// no preferred bred gender, i.e. 50/50 bred chance, so have half the probability / twice the effort to get desired instance
return new BredPalReference(gameSettings, Pal, Parent1, Parent2, EffectiveTraits)
else
{
AvgRequiredBreedings = AvgRequiredBreedings * 2,
Gender = gender,
TraitsProbability = TraitsProbability,
};
// no preferred bred gender, i.e. 50/50 bred chance, so have half the probability / twice the effort to get desired instance
return new BredPalReference(gameSettings, Pal, Parent1, Parent2, EffectiveTraits)
{
AvgRequiredBreedings = AvgRequiredBreedings * 2,
Gender = gender,
TraitsProbability = TraitsProbability,
};
}
}
else
{
Expand All @@ -319,6 +342,16 @@ public IPalReference WithGuaranteedGender(PalDB db, PalGender gender)
}
}

private ConcurrentDictionary<PalGender, IPalReference> cachedGuaranteedGenders = null;
public IPalReference WithGuaranteedGender(PalDB db, PalGender gender)
{
if (this.Gender != PalGender.WILDCARD) throw new Exception("Cannot change gender of bred pal with an already-guaranteed gender");

if (cachedGuaranteedGenders == null) cachedGuaranteedGenders = new ConcurrentDictionary<PalGender, IPalReference>();

return cachedGuaranteedGenders.GetOrAdd(gender, (gender) => WithGuaranteedGenderImpl(db, gender));
}

public override string ToString() => $"Bred {Gender} {Pal} w/ ({EffectiveTraits.TraitsListToString()})";

public override bool Equals(object obj)
Expand Down
3 changes: 2 additions & 1 deletion PalCalc.Solver/WorkingSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public bool Process(Func<List<(IPalReference, IPalReference)>, IEnumerable<IPalR
{
if (remainingWork.Count == 0) return false;

var newResults = doWork(remainingWork);
logger.Debug("beginning work processing");
var newResults = doWork(remainingWork).ToList();

// since we know the breeding effort of each potential instance, we can ignore new instances
// with higher effort than existing known instances
Expand Down

0 comments on commit dbe5800

Please sign in to comment.