Beispiel #1
0
        //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));
        }
Beispiel #2
0
        /*
         * 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);
        }
Beispiel #3
0
        /*
         * 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);
        }