//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); }
//Generates a world with a specific count of regions and items //First generates many worlds to determine an average complexity then returns a world generated with a certain tolerance of that complexity //This takes a while to run, mainly because each complexity calculation takes ~2 seconds due to running the external python script static string GenerateWorld(int regioncount, int itemcount) { double goalcomplexity = AverageComplexity(regioncount, itemcount).Average(x => x.top50); //Now generate worlds until one is generated within a certain tolerance of the average double tolerance = .10; //10% while (true) { //Generate a world, check its complexity WorldGenerator generator = new WorldGenerator(regioncount, itemcount); WorldGraph generated = generator.Generate(); int test = generated.GetLocationCount(); double complexity = generator.GetComplexity().top50; if (goalcomplexity * (1 - tolerance) < complexity && complexity < goalcomplexity * (1 + tolerance)) { //Once complexity within x% of average has been generated, return json of the world so it can be saved return(generated.ToJson()); } } }
//Generate a random worldgraph using the specified number of regions and item list public WorldGraph Generate() { HashSet <Region> regions = new HashSet <Region>(); //Set of all regions in the world for (int i = 0; i < Regions; i++) { Region r = new Region("Region-" + i.ToString()); //Each region is named Region_x. So Region-1, Region-2, etc. regions.Add(r); } //Not must loop through each region to add exits //Separate loop from the previous so that all regions are available to add as exits foreach (Region r in regions) { //The first region has some specific conditions: // 1. First exit is guaranteed to have no requirement and goes to hub region // 2. There is a guaranteed second exit, that will have a single item requirement // 3. There is a 50% chance to have a third exit, which has a 50% chance between a single item and no item if (r.Name == "Region-0") { //Add exit to hub region with no requirement List <string> currentexits = new List <string>(); Region hub = regions.First(x => x.Name == "Region-1"); AddExitsNoRequirement(regions, r, hub); currentexits.Add("Region-1"); //Add exit to 2nd region with single requirement Region second = GetRandomAvailableRegion(regions, r, currentexits); AddExitsOneRequirement(regions, r, second); currentexits.Add(second.Name); //50% chance to add a third region int random = rng.Next(1, 3); //Either 1 or 2 if (random == 2) { Region third = GetRandomAvailableRegion(regions, r, currentexits); random = rng.Next(1, 3); //50% chance to have 1 requirement, 50% chance to have none if (random == 2) { AddExitsNoRequirement(regions, r, third); } else { AddExitsOneRequirement(regions, r, third); } } } //The second region is the hub region and also has some specific conditions: // 1. Will connect to 5 regions besides the start region // 2. Half of its exits will have no item requirement, the other half will have one else if (r.Name == "Region-1") { List <string> currentexits = new List <string>(); currentexits.Add("Region-0"); for (int i = 0; i < 5; i++) //Run for 5 iterations { Region to = GetRandomAvailableRegion(regions, r, currentexits); if (i < 2) //First 3 exits (including start region) have no requirement { AddExitsNoRequirement(regions, r, to); } else //Next 3 iterations will have 1 requirement { AddExitsOneRequirement(regions, r, to); } currentexits.Add(to.Name); } } //Every other region will have a number of exits in [1, 4], however max of 2 chosen at generation, 2 more can be added by a later region else { int ExitNum = rng.Next(1, 3); //Generate random number in [1, 2] //In case r already has exits, create a list which contains all its current exits List <string> currentexits = new List <string>(); foreach (Exit e in r.Exits) { currentexits.Add(e.ToRegionName); } while (r.Exits.Count < ExitNum) //Possible that location already has specified number of exits, no big deal if so { Region to = GetRandomAvailableRegion(regions, r, currentexits); if (!string.IsNullOrEmpty(to.Name)) { AddExits(regions, r, to); //Add exit from r to the random region currentexits.Add(to.Name); //Also add dest region to list so it does not get added twice } else //Don't want to do this if r has 0 exits, but that logic is handled in GetRandomAvailableRegion { break; } } } } //Must make sure all locations are reachable Generated = new WorldGraph("Region-0", "Goal", regions.ToHashSet(), MajorItemList); List <Region> unreachable = Generated.GetUnreachableRegions(); while (unreachable.Count > 0) //At least one reachable location { //Create a connection from a random reachable location to a random unreachable location List <Region> regionscopy = regions.ToList(); helper.Shuffle(regionscopy); Region from = regionscopy.First(x => !unreachable.Contains(x)); //Not in unreachable, so it is reachable helper.Shuffle(unreachable); Region to = unreachable.First(); //Unreachable AddExits(regions, from, to); //Add connection between two regions to join subgraphs Generated = new WorldGraph("Region-0", "Goal", regions.ToHashSet(), MajorItemList); unreachable = Generated.GetUnreachableRegions(); //Recompute reachability } //Now before adding items, we will get the last region and place the goal there- No other items will be placed there Generated = new WorldGraph("Region-0", "Goal", regions.ToHashSet(), MajorItemList); Region goalregion = Generated.Regions.Last(); Item goalitem = new Item("Goal", 3); //Create goal item Location goallocation = new Location("Final Boss", "None", goalitem); //Create location for goal item with no requirement since entrance to region will have full requirement //We want all exits to the goal region to require every item so that they will all be required to complete the game string fullrequirement = ""; foreach (Item i in MajorItemList) { fullrequirement += i.Name + " and "; } fullrequirement = fullrequirement.Substring(0, fullrequirement.Length - 5); //Remove final " and " regions.First(x => x == goalregion).Locations.Add(goallocation); foreach (Exit e in regions.First(x => x == goalregion).Exits) { e.Requirements = fullrequirement; } //Must also write to requirements leading into final region foreach (Region r in regions) { foreach (Exit e in r.Exits) { if (e.ToRegionName == goalregion.Name) { e.Requirements = fullrequirement; } } } //Finally, generate item locations and place the location in the region foreach (Region r in regions) { //The starting region has some specific conditions: // 1. Three locations with no requirement // 2. 50% chance of a 4th location with one requirement if (r.Name == "Region-0") { int random = rng.Next(3, 5); for (int i = 0; i < random; i++) { if (i < random - 1) //Guaranteed 3 locations with no requirement { Location l = new Location("Region-0_Location-" + i.ToString(), "None", new Item()); regions.First(x => x == r).Locations.Add(l); //Add generated location to region } else //Possible 4th location, have 1 requirement { Location l = new Location("Region-0_Location-" + i.ToString(), GenerateOneRandomRequirement(), new Item()); regions.First(x => x == r).Locations.Add(l); //Add generated location to region } } } //The second region is the hub region and also has some specific conditions: // 1. Two locations with no requirement // 2. One location with one requirement // 3. One location with two requirements else if (r.Name == "Region-1") { for (int i = 0; i < 4; i++) { if (i < 2) { Location l = new Location("Region-1_Location-" + i.ToString(), "None", new Item()); regions.First(x => x == r).Locations.Add(l); //Add generated location to region } else if (i == 2) { Location l = new Location("Region-1_Location-" + i.ToString(), GenerateOneRandomRequirement(), new Item()); regions.First(x => x == r).Locations.Add(l); //Add generated location to region } else if (i == 3) { Location l = new Location("Region-1_Location-" + i.ToString(), GenerateTwoRandomRequirements(), new Item()); regions.First(x => x == r).Locations.Add(l); //Add generated location to region } } } //Every other region will generate 2 to 4 locations, unless region contains goal, in which case we want that to be the only location in that region else if (r != goalregion) { //Generate 2 to 4 locations per region int random = rng.Next(2, 5); for (int i = 0; i < random; i++) { //Generate a location with: // Name: Region-x_Location-y, ex Region-5_Location-2 // Requirement: Randomly Generated // Item: null item Location l = new Location(r.Name + "_Location-" + i.ToString(), GenerateRandomRequirement(false), new Item()); regions.First(x => x == r).Locations.Add(l); //Add generated location to region } } } //Now that we have a total number of regions and a count of major items, must generate junk items to fill out the item list List <Item> ItemList = MajorItemList; //Copy major item list and add goal item ItemList.Add(goalitem); Generated = new WorldGraph("Region-0", "Goal", regions.ToHashSet(), ItemList.OrderByDescending(x => x.Importance).ThenBy(x => x.Name).ToList()); //Remake generated now that items have been added int locationcount = Generated.GetLocationCount(); //Get location count and find difference so we know how many junk items to generate int difference = locationcount - MajorItemList.Count(); for (int i = 0; i < difference; i++) { //For a junk item, importance will be either 0 or 1, so generate one of those numbers randomly int importance = rng.Next(0, 2); Item newitem = new Item("JunkItem" + importance.ToString(), importance); //Name will either be JunkItem0 or JunkItem1 ItemList.Add(newitem); } Generated = new WorldGraph("Region-0", "Goal", regions.ToHashSet(), ItemList.OrderByDescending(x => x.Importance).ThenBy(x => x.Name).ToList()); //Remake generated now that items have been added return(Generated); }
/* * 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); }
//Experiment space static void Main(string[] args) { Fill filler = new Fill(); Search searcher = new Search(); Statistics stats = new Statistics(); //string testjsontext = File.ReadAllText("../../../WorldGraphs/World3.json"); //WorldGraph testworld = JsonConvert.DeserializeObject<WorldGraph>(testjsontext); //double[] testaverages = new double[5]; //for (int regioncount = 10; regioncount <= 50; regioncount += 5) //{ // for (int itemcount = 5; itemcount <= Math.Min(regioncount, 30); itemcount += 5) // { // List<TestComplexityOutput> complexity = AverageComplexity(regioncount, itemcount); // Console.WriteLine("Regions: " + regioncount + ", Items: " + itemcount); // Console.WriteLine("Sum: " + complexity.Average(x => x.sum)); // Console.WriteLine("Avg: " + complexity.Average(x => x.average)); // Console.WriteLine("Max: " + complexity.Average(x => x.max)); // Console.WriteLine("SOS: " + complexity.Average(x => x.sumofsquares)); // Console.WriteLine("Avg50: " + complexity.Average(x => x.top50)); // Console.WriteLine("Avg75: " + complexity.Average(x => x.top75)); // Console.Write(Environment.NewLine); // } //} //string generatedjson = GenerateWorld(50, 30); //string jsontest = File.ReadAllText("../../../WorldGraphs/World5.json"); //WorldGraph testworld = JsonConvert.DeserializeObject<WorldGraph>(jsontest); //int testlocationcount = testworld.GetLocationCount(); //Search testsearcher = new Search(); //testsearcher.PathsToRegion(world, world.Regions.First(x => x.Name == "Waterfall")); //Parser testparse = new Parser(); //string result = testparse.Simplify("(Sword and Bow and Bow) or Has(Key,2)"); //Should be simplified to something like (Sword and Bow) or Has(Key,2) //string result2 = testparse.Simplify("Sword or Sword and Bow"); //Should be simplified to Sword ////majoritempool.RemoveAt(8); ////majoritempool.RemoveAt(0); //bool result = testparse.RequirementsMet("(Sword and Bow) or Has(Key,2)", majoritempool); //string testjsontext = File.ReadAllText("../../../WorldGraphs/TestWorldOriginal.json"); //WorldGraph testworld = JsonConvert.DeserializeObject<WorldGraph>(testjsontext); //SphereSearchInfo testoutput = searcher.SphereSearch(testworld); //Print_Spheres(testoutput); string[] algos = { "Random", "Forward", "Assumed" }; foreach (string worldname in testworlds) { DateTime expstart = DateTime.Now; string jsontext = File.ReadAllText("../../../WorldGraphs/" + worldname + ".json"); WorldGraph world = JsonConvert.DeserializeObject <WorldGraph>(jsontext); int l = world.GetLocationCount(); //Loop to perform fill for (int i = 0; i < 3; i++) //0 = Random, 1 = Forward, 2 = assumed { if (dotests[i]) { //List<InterestingnessOutput> intstats = new List<InterestingnessOutput>(); //double totaltime = 0; int savecounter = 0; int countofexp = db.Results.Count(x => x.Algorithm == algos[i] && x.World == worldname); //int countofexp = 0; while (countofexp < trials) //Go until there are trial number of records in db { InterestingnessOutput intstat = new InterestingnessOutput(); double difference = -1; while (true) //If something goes wrong in playthrough search, may need to retry { WorldGraph input = world.Copy(); //Copy so that world is not passed by reference and overwritten List <Item> majoritempool = input.Items.Where(x => x.Importance == 2).ToList(); List <Item> minoritempool = input.Items.Where(x => x.Importance < 2).ToList(); WorldGraph randomizedgraph = new WorldGraph(); DateTime start = DateTime.Now; //Start timing right before algorithm //Decide which algo to use based on i switch (i) { case 0: randomizedgraph = filler.RandomFill(input, majoritempool); break; case 1: randomizedgraph = filler.ForwardFill(input, majoritempool); break; case 2: randomizedgraph = filler.AssumedFill(input, majoritempool); break; } randomizedgraph = filler.RandomFill(randomizedgraph, minoritempool); //Use random for minor items always since they don't matter //Calculate metrics DateTime end = DateTime.Now; difference = (end - start).TotalMilliseconds; //totaltime += difference; //string randomizedjson = JsonConvert.SerializeObject(randomizedgraph); //SphereSearchInfo output = searcher.SphereSearch(randomizedgraph); //Print_Spheres(output); try { intstat = stats.CalcDistributionInterestingness(randomizedgraph); break; //Was successful, continue } catch { } //Something went wrong, retry fill from scratch } //intstats.Add(intstat); //Store result in database Result result = new Result(); result.Algorithm = algos[i]; result.World = worldname; result.Completable = intstat.completable; result.ExecutionTime = difference; result.Bias = intstat.bias.biasvalue; result.BiasDirection = intstat.bias.direction; result.Interestingness = intstat.interestingness; result.Fun = intstat.fun; result.Challenge = intstat.challenge; result.Satisfyingness = intstat.satisfyingness; result.Boredom = intstat.boredom; db.Entry(result).State = EntityState.Added; savecounter++; if (savecounter >= 1000) //Save every 1000 results processed { db.SaveChanges(); savecounter = 0; } countofexp++; } //double avgint = intstats.Where(x => x.completable).Average(x => x.interestingness); //Console.WriteLine("Average interestingness for " + algos[i] + " Fill in world " + worldname + ": " + avgint); //double avgbias = intstats.Where(x => x.completable).Average(x => x.bias.biasvalue); //Console.WriteLine("Average bias for " + algos[i] + " Fill in world " + worldname + ": " + avgbias); //double avgfun = intstats.Where(x => x.completable).Average(x => x.fun); //Console.WriteLine("Average fun for " + algos[i] + " Fill in world " + worldname + ": " + avgfun); //double avgchal = intstats.Where(x => x.completable).Average(x => x.challenge); //Console.WriteLine("Average challenge for " + algos[i] + " Fill in world " + worldname + ": " + avgchal); //double avgsat = intstats.Where(x => x.completable).Average(x => x.satisfyingness); //Console.WriteLine("Average satisfyingness for " + algos[i] + " Fill in world " + worldname + ": " + avgsat); //double avgbore = intstats.Where(x => x.completable).Average(x => x.boredom); //Console.WriteLine("Average boredom for " + algos[i] + " Fill in world " + worldname + ": " + avgbore); //double avgtime = totaltime / trials; //Console.WriteLine("Average time to generate for " + algos[i] + " Fill in world " + worldname + ": " + avgtime + "ms"); db.SaveChanges(); //Save changes when combo of algo and world is done } //Console.Write(Environment.NewLine); } //Console.Write(Environment.NewLine); //Console.Write(Environment.NewLine); DateTime expend = DateTime.Now; double expdifference = (expend - expstart).TotalMinutes; Console.WriteLine("Time to perform " + trials + " iterations for world " + worldname + ": " + expdifference + " minutes"); } Console.ReadLine(); }