//Calculates the bias for a given permutation of items in the world graph public BiasOutput CalcDistributionBias(WorldGraph world) { //Get total counts for majors and items so a percent can be calculated int totalmajorcount = world.Items.Where(x => x.Importance >= 2).Count(); int totallocationcount = world.GetLocationCount(); //Find spheres in randomized world Search searcher = new Search(); List <WorldGraph> spheres = searcher.SphereSearch(world).Spheres; //Initialize variables to use in the loop double[] spherebias = new double[spheres.Count]; int rollingmajorcount = 0; int rollinglocationcount = 0; for (int i = 0; i < spheres.Count; i++) { WorldGraph sphere = spheres[i]; //Check the number of major items in the sphere, not counting those in previous spheres int majoritemcount = sphere.CollectMajorItems().Count - rollingmajorcount; rollingmajorcount += majoritemcount; //Check the number of locations in the sphere, not counting those in previous spheres int locationcount = sphere.GetLocationCount() - rollinglocationcount; rollinglocationcount += locationcount; //Find the percentage of major items and locations in this sphere double majorpercent = majoritemcount / (double)totalmajorcount; double locationpercent = locationcount / (double)totallocationcount; //Now find the difference between the two percentages double difference = majorpercent - locationpercent; spherebias[i] = difference; } //Now that we have a list of biases find the sum of their absolute values to determine absolute bias //Also use the positivity of bias before and after the median to determine bias direction double overallsum = 0; double beforesum = 0; //Sums bias before median so a bias direction can be computed double aftersum = 0; //Sums bias after median so a bias direction can be computed bool even = spherebias.Length % 2 == 0; //Want to check if even so can determine when after the median is int median = spherebias.Length / 2; //Use median to determine bias direction for (int i = 0; i < spherebias.Length; i++) { overallsum += Math.Abs(spherebias[i]); if (i < median) //Before median, add to that sum { beforesum += spherebias[i]; } else if ((i >= median && even) || (i > median && !even)) //After median, add to that sum. If it's even then >= makes sense so every index is checked, if odd then skip middle { aftersum = spherebias[i]; } } //Package output and return BiasOutput output = new BiasOutput(); output.biasvalue = overallsum / spherebias.Length; //Get average of absolute value to determine overall bias output.direction = beforesum < aftersum; //If bias is more positive before the median, the direction is toward the beginning, otherwise toward end return(output); }
//Calculate info about human-like playthrough and then return the score public InterestingnessOutput CalcDistributionInterestingness(WorldGraph world) { PlaythroughInfo info = new PlaythroughInfo(); try { info = searcher.PlaythroughSearch(world.Copy()); } catch { throw new Exception(); //Something went wrong, have calling code retry } BiasOutput biasinfo = CalcDistributionBias(world); return(ScorePlaythrough(world, info, biasinfo)); }
/* * Score info about human-like playthrough * Several considerations: * 1. Number of locations collected for each region traversed * 2. Number of regions traversed between finding major or helpful items * 3. Number of regions traversed between finding major items */ public InterestingnessOutput ScorePlaythrough(WorldGraph world, PlaythroughInfo input, BiasOutput biasinfo) { //First, calculate fun metric, which desires a consistently high rate of checking item locations Queue <int> RollingAvg = new Queue <int>(); List <double> avgs = new List <double>(); List <bool> highavg = new List <bool>(); foreach (int num in input.LocationsPerTraversal) { if (RollingAvg.Count == 5) //Rolling average of last 5 values { RollingAvg.Dequeue(); } RollingAvg.Enqueue(num); double avg = RollingAvg.Average(); highavg.Add(avg >= 1); //If average is above 1, considered high enough to be fun, so add true to list, else add false avgs.Add(avg); } double fun = (double)highavg.Count(x => x) / highavg.Count(); //Our "Fun" score is the percentage of high values in the list //Next calculate challenge metric, which desires rate at which items are found to be within some optimal range so that it is not too often or too rare double LocationToItemRatio = (double)world.GetLocationCount() / world.Items.Where(x => x.Importance == 2).Count(); int low = (int)Math.Floor(LocationToItemRatio * .5); int high = (int)Math.Ceiling(LocationToItemRatio * 1.5); RollingAvg = new Queue <int>(); avgs = new List <double>(); List <bool> avginrange = new List <bool>(); foreach (int num in input.BetweenMajorList) { if (RollingAvg.Count == 3) //Tighter rolling average of last 3 values { RollingAvg.Dequeue(); } RollingAvg.Enqueue(num); double avg = RollingAvg.Average(); avginrange.Add(low <= avg && avg <= high); //If value is within range rather than too high or too low, add true to list to indicate it is within a good range avgs.Add(avg); } double challenge = (double)avginrange.Count(x => x) / avginrange.Count(); //Our "Challenge" score is the percentage of values in the list within desirable range //Next calculate satisfyingness metric based on how many locations are unlocked when an item is found double LocationToItemRatioWithoutInitial = (double)(world.GetLocationCount() - input.InitialReachableCount) / world.Items.Where(x => x.Importance == 2).Count(); int satthreshold = (int)Math.Floor(LocationToItemRatioWithoutInitial); //Set threshold as number of not-immediately-accessible locations divided by number of major items List <bool> SatisfyingReachesThreshold = new List <bool>(); foreach (int num in input.LocationsUnlockedPerMajorFound) { SatisfyingReachesThreshold.Add(num >= satthreshold); } double satisfyingness = (double)SatisfyingReachesThreshold.Count(x => x) / SatisfyingReachesThreshold.Count(); //Our "Satisfyingness" score is the percentage of values above the desired threshold //Finally calculate boredom by observing regions which were visited more often than is expected //First get a count of how many times each region was visited List <int> visitcounts = new List <int>(); foreach (Region r in world.Regions) { visitcounts.Add(input.Traversed.Count(x => x.Name == r.Name)); } //Calculate threshold with max number of times region should be visited being the number of traversals divided by number of regions double TraversedToRegionRatio = (double)input.Traversed.Count() / world.Regions.Count(); int borethreshold = (int)Math.Ceiling(TraversedToRegionRatio); List <bool> VisitsAboveThreshold = new List <bool>(); foreach (int num in visitcounts) { VisitsAboveThreshold.Add(num > borethreshold); //Again as before, add list of bool when value is above threshold } double boredom = (double)VisitsAboveThreshold.Count(x => x) / VisitsAboveThreshold.Count(); //Our "Boredom" score is the percentage of values above the desired threshold //Add calculated stats to output. If a result is NaN (possible when not completable) save as -1 InterestingnessOutput output = new InterestingnessOutput(); output.bias = biasinfo; output.fun = double.IsNaN(fun) ? -1 : fun; output.challenge = double.IsNaN(challenge) ? -1 : challenge; output.satisfyingness = double.IsNaN(satisfyingness) ? -1 : satisfyingness; output.boredom = double.IsNaN(boredom) ? -1 : boredom; //Use stats to calculate final interestingness score //Each score is a double in the range [0, 1] //Multiply each score (or its 1 - score if low score is desirable) by its percentage share of the total double biasscore = (1 - output.bias.biasvalue) * .2; double funscore = output.fun * .2; double challengescore = output.challenge * .2; double satscore = output.satisfyingness * .2; double borescore = (1 - output.boredom) * .2; double intscore = biasscore + funscore + challengescore + satscore + borescore; //If any components are NaN, consider interestingness as NaN as well if (double.IsNaN(fun) || double.IsNaN(challenge) || double.IsNaN(satisfyingness) || double.IsNaN(boredom)) { output.interestingness = -1; } else { output.interestingness = intscore; } output.completable = input.Completable; return(output); }