//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); }
/* * The goal of this function is to traverse the game world like a player of the game rather than like an algorithm. * It's assumed that the player has decent knowledge of the game, meaning they know where locations are and * wether or not those locations and regions are accessible. * Therefore a heuristic is used to score each posisble exit a player could take based on number of item locations * and how close they are to the current location. * Whichever exit has the maximum score (meaning maximum potential to gain new items) is taken. * We keep several counts which can be used to gauge interestingness of a seed: * 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 PlaythroughInfo PlaythroughSearch(WorldGraph world) { List <int> BetweenMajorOrHelpfulList = new List <int>(); //This did not end up being utilized. List <int> BetweenMajorList = new List <int>(); List <int> LocationsPerTraversal = new List <int>(); List <int> LocationsUnlockedPerMajorFound = new List <int>(); Region current = world.Regions.First(x => x.Name == world.StartRegionName); Region previous = new Region(); List <Item> owneditems = new List <Item>(); List <Region> traversed = new List <Region>(); int BetweenMajorOrHelpfulCount = 0; int BetweenMajorCount = 0; bool gomode = false; int prevlocationsunlocked = GetReachableLocations(world, owneditems).GetLocationCount(); //Initial count of unlocked locations int initial = prevlocationsunlocked; //Saved to add to output later while (owneditems.Count(x => x.Importance == 3) < 1) //Loop until goal item found, or dead end reached (see break statement below) { //If count gets this high usually indicates search is stuck in a loop... not great but just retry with a new permutation... //Seems to happen roughly one in every 5000 permutations of most complex world (World5) so fairly rare occurrence if (traversed.Count > world.Regions.Count() * 20) { throw new Exception(); } traversed.Add(current); int checkcount = 0; int prevcount = -1; while (prevcount < owneditems.Count()) //While loop to re-check locations if major item is found in this region { prevcount = owneditems.Count(); //First, check each location in the current region, if accessible and not already searched check it for major items foreach (Location l in current.Locations) { if (l.Item.Importance > -1 && parser.RequirementsMet(l.Requirements, owneditems)) { checkcount++; //Add to check count Item i = l.Item; l.Item = new Item(); //Remove item, location importance set to -1 if (i.Importance == 1) //Helpful item, did not end up being utilized. { //Update helpful list only and reset counts BetweenMajorOrHelpfulList.Add(BetweenMajorOrHelpfulCount); BetweenMajorOrHelpfulCount = 0; } else if (i.Importance == 2) //Major item { owneditems.Add(i); //Collect item //Update both lists and reset counts BetweenMajorOrHelpfulList.Add(BetweenMajorOrHelpfulCount); BetweenMajorList.Add(BetweenMajorCount); BetweenMajorOrHelpfulCount = 0; BetweenMajorCount = 0; //Find number of locations unlocked, add to list, update count of locations unlocked int locationsunlocked = GetReachableLocations(world, owneditems).GetLocationCount(); int newlocations = locationsunlocked - prevlocationsunlocked; LocationsUnlockedPerMajorFound.Add(newlocations); prevlocationsunlocked = locationsunlocked; } else if (i.Importance == 3) //Goal item, break loop here { owneditems.Add(i); //Collect goal item, indicates successful completion } } } } if (!gomode) //Update this traversal with the number of locations checked within it, unless player is in go mode { LocationsPerTraversal.Add(checkcount); } List <double> exitscores = new List <double>(); if (current.Exits.Count == 1) //Only one exit, take it unless need to break { double score = ExitScore(world, owneditems, current, traversed, current.Exits.First()); if (score > 0) //If score is -1 and this is the only exit, then a dead end has been reached; if score is 0, then there are no items left to find; otherwise simply take exit since it is the only one { current = world.Regions.First(x => x.Name == current.Exits.First().ToRegionName); //Move to region if (score >= 10000000000) //Score this high indicates player is in "go mode" where they are now rushing the end of the game { gomode = true; } //Update count of regions between finding items BetweenMajorOrHelpfulCount++; BetweenMajorCount++; } else { break; //Break loop in failure } } else { List <double> scores = new List <double>(); //Calculate score for each exit foreach (Exit e in current.Exits) { scores.Add(ExitScore(world, owneditems, current, traversed, e)); } if (scores.Count(x => x > 0) > 0) //If none of the scores are greater than 0, all exits are either untraversable or have no available items, indicating dead end has been reached { int maxindex = scores.IndexOf(scores.Max()); //Get index of the maximum score previous = current; current = world.Regions.First(x => x.Name == current.Exits.ElementAt(maxindex).ToRegionName); //Move to region with maximum score if (scores.Max() >= 10000000000) //Score this high indicates player is in "go mode" where they are now rushing the end of the game { gomode = true; } //Update count of regions between finding items BetweenMajorOrHelpfulCount++; BetweenMajorCount++; } else { break; //Break loop in failure } } } //Package all lists into list of lists and return PlaythroughInfo output = new PlaythroughInfo(); output.BetweenMajorOrHelpfulList = BetweenMajorOrHelpfulList; output.BetweenMajorList = BetweenMajorList; output.LocationsPerTraversal = LocationsPerTraversal; output.LocationsUnlockedPerMajorFound = LocationsUnlockedPerMajorFound; output.Traversed = traversed; output.Completable = owneditems.Count(x => x.Importance == 3) > 0; //Has goal item, so game is completable output.InitialReachableCount = initial; return(output); }