/* * This algorithm begins by assuming the player has access to all items, which implies all * locations are reachable, so R is initialized to the entire game and I contains all items. A random * item is selected from I and is removed from the player’s assumed item list, meaning that some * locations may become unreachable, and removed from R. A random, empty location which is * still reachable will then be selected and the previously removed item will be placed there. The * way this algorithm initially assumes all items are available and slowly removes them reducing * reachable areas, it can be thought of as a reverse fill. */ public WorldGraph AssumedFill(WorldGraph world, List <Item> itempool) { List <Item> owneditems = itempool; //In contrast to other two algos, I is initialized to all items and itempool is empty itempool = new List <Item>(); WorldGraph reachable = searcher.GetReachableLocationsAssumed(world, owneditems); //Initially R should equal all locations in the game List <Location> reachablelocations = reachable.GetAllEmptyLocations(); helper.Shuffle(owneditems); while (reachablelocations.Count > 0 && owneditems.Count > 0) { Item item = helper.Pop(owneditems); //Pop random item from I, R will shrink helper.Shuffle(owneditems); reachable = searcher.GetReachableLocationsAssumed(world, owneditems); //Recalculate R now that less items are owned reachablelocations = reachable.GetAllEmptyLocations(); //Get empty locations which are reachable helper.Shuffle(reachablelocations); if (reachablelocations.Count() == 0) //If this happens, means there are no reachable locations left and must return, usually indicates uncompletable permutation { break; } Location location = helper.Pop(reachablelocations); //Remove location from list world.Place(location, item); //Place random item in random location itempool.Add(item); //Add item to item pool } return(world); //World has been filled with items, return }
//Utilizes the recusive DFS for exit score function to return a score for this exit if its requirement is met, otherwise returns -1 private double ExitScore(WorldGraph world, List <Item> owneditems, Region current, List <Region> traversed, Exit exit) { if (parser.RequirementsMet(exit.Requirements, owneditems)) { double score = 0; List <Region> visited = new List <Region>(); visited.Add(current); //Do this so path does not go through current region Region exitto = world.Regions.First(x => x.Name == exit.ToRegionName); //Get the region this edge leads to maxtraversed = 0; score += RecursiveDFSForExitScore(world, exitto, visited, owneditems, 1); //Add score from a recursive search which scores item locations, scored lower more regions traversed int multiplier = maxtraversed == 1 ? 2 : 1; //Give a multiplier if this edge leads to a single, dead-end region //Give divider based on how recently the region was visited int divider = 1; int lastindex = traversed.FindLastIndex(x => x.Name == exit.ToRegionName); if (lastindex > -1) { int howrecent = traversed.Count - 2 - lastindex; divider = 16 - howrecent; //Most recent region divided by 8, 2nd most recent divided by 7, etc to minimum of 1 divider = divider < 1 ? 1 : divider; } return(score * multiplier / divider); } else //Can not traverse exit { return(-1); //Return -1 to indicate it cannot be crossed } }
/* * Sphere Search is done iteratively in “Spheres” and is used to attempt to trace a path * from the beginning to the end of the game. The first sphere s is simply all locations which are * reachable from the beginning of the game. As it searches these locations, it adds key items * found to a temporary set; we do not want those items to affect reachability until the next sphere * iteration so we do not yet add them to I. After all reachable locations have been found, sphere s * is added to the list of spheres S, and all items in the temporary set are added to I. It then * iterates again with a new sphere s. */ public SphereSearchInfo SphereSearch(WorldGraph world) { SphereSearchInfo output = new SphereSearchInfo(); output.Spheres = new List <WorldGraph>(); List <Item> owneditems = new List <Item>(); //Initial sphere s0 includes items reachable from the start of the game WorldGraph s0 = GetReachableLocations(world, owneditems); owneditems = s0.CollectMajorItems(); //Collect all major items reachable from the start of the game output.Spheres.Add(s0); //Add initial sphere to sphere list //sx indicates every sphere after the first. Any major items found in s0 means sx should be bigger. WorldGraph sx = GetReachableLocations(world, owneditems); int temp = owneditems.Where(x => x.Importance >= 2).Count(); //Temp is the count of previously owned major items owneditems = sx.CollectAllItems(); //Used to find new count of major items //If counts are equal then no new major items found, stop searching while (owneditems.Where(x => x.Importance >= 2).Count() > temp) //If new count is not greater than old count, that means all currently reachable locations have been found { output.Spheres.Add(sx); //If new locations found, add to sphere list //Take the same steps taken before the loop: Get new reachable locations, collect new major items, and check to see if new count is larger than old count sx = GetReachableLocations(world, owneditems); temp = owneditems.Where(x => x.Importance >= 2).Count(); //Only want to consider count of major items owneditems = sx.CollectAllItems(); } //At this point, either a dead end has been found or all locations have been discovered //If the goal item is in the list of owned items, means the end has been found and thus the game is completable output.Completable = owneditems.Count(x => x.Name == world.GoalItemName) > 0; return(output); }
//Constructor from a previous world graph //Copies the start region and goal but new items and regions //Used when constructing partial graph of world public WorldGraph(WorldGraph world) { StartRegionName = world.StartRegionName; GoalItemName = world.GoalItemName; Regions = new HashSet <Region>(); Items = new List <Item>(); }
public WorldGraph Generated; //Result of world generation //Constructor which only specifies a number of items which are then generated public WorldGenerator(int regions, int items) { rng = new Random(); helper = new Helpers(); Regions = regions; MajorItemList = GenItemList(items); Generated = new WorldGraph(); }
//Constructor which specifies an item list public WorldGenerator(int regions, List <Item> itemlist) { rng = new Random(); helper = new Helpers(); Regions = regions; MajorItemList = itemlist; Generated = new WorldGraph(); }
//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); }
//Utilizes BFS search algorithm to find all locations in the world which are reachable with the current item set //In this algorithm, we want to check for items which have been removed from I but are still contained within R, so an initial search is done to collect items, //then repeated iteratively until no new items are found, at which point the final reachability graph is returned. public WorldGraph GetReachableLocationsAssumed(WorldGraph world, List <Item> owneditems) { WorldGraph copy = world.Copy(); //Used so items may be removed from world at will List <Item> newitems = ItemSearch(copy, owneditems); //Find items within R List <Item> combined = owneditems.ToList(); //Copy list while (newitems.Count > 0) { combined.AddRange(newitems); //Add items to currently used items newitems = ItemSearch(copy, combined); //Find items within R } return(GetReachableLocations(world, combined)); //Use that combined list to find final search result }
//Calculate the average complexity from generating many worlds with a specific regioncount and itemcount static List <TestComplexityOutput> AverageComplexity(int regioncount, int itemcount) { //First do x trials to determine an average complexity int gentrials = 5; List <TestComplexityOutput> outputs = new List <TestComplexityOutput>(); for (int i = 0; i < gentrials; i++) { WorldGenerator generator = new WorldGenerator(regioncount, itemcount); WorldGraph generated = generator.Generate(); outputs.Add(generator.GetComplexity()); } return(outputs); //Determine average complexity, we want the goal complexity to be within some% of this }
private List <List <Region> > paths = new List <List <Region> >(); //Declared outside of function scope so multiple instances of following two functions can access //Use DFS to find all possible paths from the root to the specified region //Not including paths that go back on themselves public List <List <Region> > PathsToRegion(WorldGraph world, Region dest) { Region root = world.Regions.First(x => x.Name == world.StartRegionName); List <Region> visited = new List <Region>(); paths = new List <List <Region> >(); if (root == dest) //If root and dest equal, return empty list { return(paths); } RecursiveDFSForPathList(world, root, dest, visited); //Recursively run DFS, when dest found add the path to paths var return(paths); //Return list of paths }
//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)); }
//Initially, went to collect all items which are reachable with the current item set and not already contained within the item set public List <Item> ItemSearch(WorldGraph world, List <Item> owneditems) { List <Item> newitems = new List <Item>(); Region root = world.Regions.First(x => x.Name == world.StartRegionName); Queue <Region> Q = new Queue <Region>(); HashSet <Region> visited = new HashSet <Region>(); Q.Enqueue(root); visited.Add(root); //Implementation of BFS while (Q.Count > 0) { Region r = Q.Dequeue(); foreach (Exit e in r.Exits) { //Normally in BFS, all exits would be added //But in this case, we only want to add exits which are reachable if (parser.RequirementsMet(e.Requirements, owneditems)) { Region exitto = world.Regions.First(x => x.Name == e.ToRegionName); //Get the region this edge leads to if (!visited.Contains(exitto)) //Don't revisit already visited nodes on this path { Q.Enqueue(exitto); visited.Add(exitto); } } } //Subsearch to check each edge to a location in the current region //If requirement is met, add it to reachable locations foreach (Location l in r.Locations) { if (parser.RequirementsMet(l.Requirements, owneditems)) { if (l.Item.Importance == 2) //If location contains a major item { newitems.Add(l.Item); l.Item = new Item(); //Remove item so it isn't added again in future iterations } } } } return(newitems); }
//Recursively check exits with copy of visited list //It's done this way so that after the destination or a dead end is met, the code flow "backs up" public void RecursiveDFSForPathList(WorldGraph world, Region r, Region dest, List <Region> visited) { visited.Add(r); //Add to visited list if (r == dest) { paths.Add(visited); //If this is the dest, then visited currently equals a possible path return; } foreach (Exit e in r.Exits) { Region exitto = world.Regions.First(x => x.Name == e.ToRegionName); //Get the region this edge leads to if (!visited.Contains(exitto)) //Don't revisit already visited nodes on this path { List <Region> copy = new List <Region>(visited); //If don't do this List is passed by reference, algo doesn't work RecursiveDFSForPathList(world, exitto, dest, copy); } } }
//Utilizes BFS search algorithm to find all locations in the world which are reachable with the current item set //Important note is that throughout this function the owned items are static, they are not collected throughout (as they are in sphere search) //It is used for forward search, where there is no need to check for items currently within R, as well as other places such as sphere search public WorldGraph GetReachableLocations(WorldGraph world, List <Item> owneditems) { WorldGraph reachable = new WorldGraph(world); Region root = world.Regions.First(x => x.Name == world.StartRegionName); Queue <Region> Q = new Queue <Region>(); HashSet <Region> visited = new HashSet <Region>(); Q.Enqueue(root); visited.Add(root); //Implementation of BFS while (Q.Count > 0) { Region r = Q.Dequeue(); Region toadd = new Region(r); foreach (Exit e in r.Exits) { //Normally in BFS, all exits would be added //But in this case, we only want to add exits which are reachable if (parser.RequirementsMet(e.Requirements, owneditems)) { toadd.Exits.Add(e); Region exitto = world.Regions.First(x => x.Name == e.ToRegionName); //Get the region this edge leads to if (!visited.Contains(exitto)) //Don't revisit already visited nodes on this path { Q.Enqueue(exitto); visited.Add(exitto); } } } //Subsearch to check each edge to a location in the current region //If requirement is met, add it to reachable locations foreach (Location l in r.Locations) { if (parser.RequirementsMet(l.Requirements, owneditems)) { toadd.Locations.Add(l); } } reachable.Regions.Add(toadd); //Add every reachable exit and location discovered in this iteration } return(reachable); //Return graph of reachable locations }
//Copy all properties of graph and return //Used so that input graph is not copied by reference and overwritten public WorldGraph Copy() { WorldGraph copy = new WorldGraph(); copy.StartRegionName = StartRegionName; copy.GoalItemName = GoalItemName; copy.Regions = new HashSet <Region>(); foreach (Region r in Regions) { Region rcopy = new Region(); rcopy.Name = r.Name; rcopy.Exits = new HashSet <Exit>(); foreach (Exit e in r.Exits) { Exit ecopy = new Exit(); ecopy.ToRegionName = e.ToRegionName; ecopy.Requirements = e.Requirements; rcopy.Exits.Add(e); } rcopy.Locations = new HashSet <Location>(); foreach (Location l in r.Locations) { Location lcopy = new Location(); lcopy.Name = l.Name; lcopy.Requirements = l.Requirements; Item icopy = new Item(); icopy.Importance = l.Item.Importance; icopy.Name = l.Item.Name; lcopy.Item = icopy; rcopy.Locations.Add(lcopy); } copy.Regions.Add(rcopy); } copy.Items = new List <Item>(); foreach (Item i in Items) { Item icopy = new Item(); icopy.Importance = i.Importance; icopy.Name = i.Name; copy.Items.Add(icopy); } return(copy); }
//G: Graph of world locations (called world in code) // A node in G initially has a null value, this value can be filled with a key item // An edge in G may require certain items to traverse //R: Graph of reachable locations (called reachable in code) //I: Set of items owned, determines R, called owneditems in code //I*: Set of items not owned, inverse of I, called itempool in code //Goal: A specific item in G which signifies the end of the game //Start: A specific region in G which the player starts in /* * This algorithm simply places a random key item in a random location until either of these * sets are empty (Usually items). After placing all a check is done to see if the game is beatable. If * not it runs the algorithm again. In complex world this could potentially take hundreds of attempts. */ public WorldGraph RandomFill(WorldGraph world, List <Item> itempool) { //Initialize owneditems to empty and locations to all that are empty List <Item> owneditems = new List <Item>(); List <Location> locations = world.GetAllEmptyLocations(); helper.Shuffle(locations); helper.Shuffle(itempool); while (locations.Count > 0 && itempool.Count > 0) { Location location = helper.Pop(locations); //Select random location helper.Shuffle(locations); Item item = helper.Pop(itempool); //Take random item from item pool helper.Shuffle(itempool); world.Place(location, item); //Place random item in random location owneditems.Add(item); //Add to owned items } return(world); //World has been filled with items, return }
//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()); } } }
/* * This algorithm initializes set R to be the reachable locations from the start of the game. It * then chooses an item from the item pool I* and places it in a random location in set R, meaning * it is also added to I. This location is then removed from consideration and all locations that * become reachable are added to R. Repeat until R or I* is empty. To be clear, items related to * progression are placed first, and then everything else is filled in with helpful or junk items. In * fact, usually the helpful and junk items are placed using random fill since it’s faster and * placement doesn’t matter. */ public WorldGraph ForwardFill(WorldGraph world, List <Item> itempool) { List <Item> owneditems = new List <Item>(); //Initialize owneditems to empty WorldGraph reachable = searcher.GetReachableLocations(world, owneditems); //Initially R should only equal locations reachable from the start of the game List <Location> locations = reachable.GetAllEmptyLocations(); helper.Shuffle(locations); helper.Shuffle(itempool); while (locations.Count > 0 && itempool.Count > 0) { Location location = helper.Pop(locations); //Get random location and item Item item = helper.Pop(itempool); helper.Shuffle(itempool); world.Place(location, item); //Place random item in random reachable location owneditems.Add(item); //Add new item to owned items, R will expand reachable = searcher.GetReachableLocations(world, owneditems); //Recalculate R now that more items are owned locations = reachable.GetAllEmptyLocations(); helper.Shuffle(locations); } return(world); //World has been filled with items, return }
//Scores an exit by recurisvely searching for items, adding score for every available location, with less weight if farther away public double RecursiveDFSForExitScore(WorldGraph world, Region r, List <Region> visited, List <Item> owneditems, int traversed) { maxtraversed = Math.Max(traversed, maxtraversed); visited.Add(r); //Add to visited list double score = 0; double multiplier = Math.Max(1 / 8, 1 / (double)traversed); //Max multiplier is 1, Minimum multiplier is 1/8 //First look at all locations in region to add to score foreach (Location l in r.Locations) { if (parser.RequirementsMet(l.Requirements, owneditems)) //Only want to consider available locations { if (l.Item.Importance == 3) //Path contains goal item, add large amount to score, add 100 / traversed so that shorter paths to goal preferred { score += 10000000000 + (100 / traversed); } else if (l.Item.Importance > -1) //Else just add 1 if item isn't already collected (remember, although player knows there is a location here, they don't know what item it is unless it's the goal) { score += 1 * multiplier; } } } //Now recursively look through each exit foreach (Exit e in r.Exits) { if (parser.RequirementsMet(e.Requirements, owneditems)) //Only consider exit if it can be traversed { Region exitto = world.Regions.First(x => x.Name == e.ToRegionName); //Get the region this edge leads to if (!visited.Contains(exitto)) //Don't revisit already visited nodes on this path { //Recursively call this function, adding 1 to traversed, score will be added to our score and returned //We purposely pass visited by reference rather than by value, ensuring that locations are only visited once score += RecursiveDFSForExitScore(world, exitto, visited, owneditems, traversed + 1); } } } return(score); }
//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(); }
//Experiment space static void Main(string[] args) { Fill filler = new Fill(); Search searcher = new Search(); Statistics stats = new Statistics(); ////Uncomment to test different complexity measures and generate many worlds with different parameters. //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); // } //} //Loop through each algorithm set to be used and each world in the list, performing specified algorithm on specified world and recording information about the result. 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); //Loop to perform fill algorithms for (int i = 0; i < 3; i++) //0 = Random, 1 = Forward, 2 = Assumed { if (dotests[i]) { int savecounter = 0; int countofexp = db.Results.Count(x => x.Algorithm == algos[i] && x.World == worldname); 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; try { intstat = stats.CalcDistributionInterestingness(randomizedgraph); break; //Was successful, continue } catch { } //Something went wrong, retry fill from scratch ////Uncomment to print the spheres of the result. //SphereSearchInfo output = searcher.SphereSearch(randomizedgraph); //Print_Spheres(output); } //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++; } db.SaveChanges(); //Save changes when combo of algo and world is done } } DateTime expend = DateTime.Now; double expdifference = (expend - expstart).TotalMinutes; Console.WriteLine("Time to perform " + trials + " iterations for world " + worldname + ": " + expdifference + " minutes"); //Print how long this world took to do } Console.ReadLine(); }
//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); }
/* * 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); }
/* * 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); }
//Calculate complexity of the base graph (Not considering items, only rules for location reachability) public TestComplexityOutput CalcWorldComplexity(WorldGraph world) { List <string> totalrules = new List <string>(); /* * For each location, calculate a total rule * Total rule meaning it includes every possible path to get there plus the rule for the location itself * ex, 2 paths to location: Region A -> Region B -> Region C, Region A -> Region C * Location has Rule X * Then the total rule will equal: * ((A->B and B->C) or (A->C)) and Rule X */ foreach (Region r in world.Regions) { //Must calculate every possible path (that doesn't go back on itself) from root to the region r List <List <Region> > paths = searcher.PathsToRegion(world, r); string regionstring = ""; if (paths.Count > 0) //If it equals 0, current region is root, do not need region string { //Go through each path and calculate the rule for that path to construct an absolute rule for the region for (int i = 0; i < paths.Count; i++) { List <Region> path = paths[i]; string pathstring = ""; for (int j = 0; j < path.Count - 1; j++) //Last region is dest, don't need to check thus paths.Count - 1 { if (j > 0) { pathstring += " and "; // Every requirement on this path must be met, so use "and" } pathstring += "(" + path[j].Exits.First(x => x.ToRegionName == path[j + 1].Name).Requirements + ")"; } if (i > 0) { regionstring += " or "; //The pathstrings are different options, so use "or" } regionstring += "(" + pathstring + ")"; } } //Now calculate the total rule for each location foreach (Location l in r.Locations) { string totalrule = ""; if (string.IsNullOrEmpty(regionstring)) //For when region is root { totalrule = l.Requirements; } else { totalrule = "(" + regionstring + ") and " + l.Requirements; //Must meet at least one path requirement to reach the region and the location requirement } totalrule = parser.Simplify(totalrule.Replace("None", "true")); //Simplifies the boolean expression totalrules.Add(totalrule); } } //We now have a list for the total rule of every location in the game //Calculate score for each rule and add them all to list List <double> scores = new List <double>(); foreach (string rule in totalrules) { scores.Add(parser.CalcRuleScore(rule)); } //Use list of scores to calculate final score and return return(ComplexityScoreCalculation(scores)); }